Dlaczego wskaźniki są tak wiodącym czynnikiem dezorientacji wielu nowych, a nawet starszych studentów szkół wyższych w C lub C ++? Czy są jakieś narzędzia lub procesy myślowe, które pomogłyby ci zrozumieć, w jaki sposób wskaźniki działają na poziomie zmiennej, funkcji i poza nią?
Jakie są dobre praktyki, które można zrobić, aby doprowadzić kogoś do poziomu „Ah-hah, rozumiem”, nie wpędzając go w ogólną koncepcję? Zasadniczo scenariusze przypominające ćwiczenia.
Odpowiedzi:
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:
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:
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.
Układ pamięci:
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ś.
Układ pamięci:
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.
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.
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:
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).
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.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.
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:
Układ pamięci po drugim przydziale:
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:
Po wykonaniu tej metody w naszych zmiennych nie ma miejsca na adres do domu, ale dom wciąż tam jest.
Układ pamięci:
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.
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:
Układ pamięci po pierwszym przydziale:
Układ pamięci po drugim przydziale:
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.
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:Układ pamięci (dodano NextHouse jako łącze w obiekcie, zaznaczone czterema LLLL na poniższym diagramie):
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:
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:
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.
źródło
Na mojej pierwszej klasie Comp Sci wykonaliśmy następujące ćwiczenie. To prawda, że była to sala wykładowa z około 200 studentami ...
Profesor pisze na tablicy:
int john;
John wstaje
Profesor pisze:
int *sally = &john;
Sally wstaje, wskazuje na Johna
Profesor:
int *bill = sally;
Bill wstaje, wskazuje na Johna
Profesor:
int sam;
Sam wstaje
Profesor:
bill = &sam;
Bill wskazuje teraz na Sama.
Myślę, że masz pomysł. Myślę, że spędziliśmy na tym około godziny, dopóki nie omówiliśmy podstawowych zasad przypisywania wskaźnika.
źródło
Pomocną dla wyjaśnienia wskaźników analogią są hiperłącza. Większość ludzi może zrozumieć, że link na stronie internetowej „prowadzi” do innej strony w Internecie, a jeśli możesz skopiować i wkleić ten hiperlink, to obie strony wskażą tę samą oryginalną stronę internetową. Jeśli przejdziesz i edytujesz tę oryginalną stronę, a następnie podążaj za jednym z tych linków (wskaźników), otrzymasz nową zaktualizowaną stronę.
źródło
int *a = b
nie tworzy dwóch kopii*b
).Powodem, dla którego wskaźniki zdają się wprowadzać w błąd tak wielu ludzi, jest to, że w większości pochodzą z niewielkim lub żadnym doświadczeniem w architekturze komputerowej. Ponieważ wielu nie zdaje sobie sprawy z tego, jak komputery (maszyna) są faktycznie realizowane - praca w C / C ++ wydaje się obca.
Ćwiczenie polega na poproszeniu ich o zaimplementowanie prostej maszyny wirtualnej opartej na kodzie bajtowym (w dowolnym wybranym przez siebie języku, Python działa w tym przypadku doskonale) z zestawem instrukcji skoncentrowanym na operacjach wskaźnika (ładowanie, przechowywanie, adresowanie bezpośrednie / pośrednie). Następnie poproś ich o napisanie prostych programów dla tego zestawu instrukcji.
Wszystko, co wymaga nieco więcej niż prostego dodania, będzie wymagało wskaźników i na pewno je otrzymają.
źródło
Koncepcja symbolu zastępczego wartości - zmiennych - odwzorowuje coś, czego uczymy się w szkole - algebry. Nie ma istniejącej równoległości, którą można narysować bez zrozumienia, w jaki sposób fizycznie układa się pamięć w komputerze, i nikt nie myśli o takich rzeczach, dopóki nie zajmą się niskimi poziomami - na poziomie komunikacji C / C ++ / bajtów .
Pola adresów. Pamiętam, kiedy uczyłem się programowania BASIC w mikrokomputerach, były tam te ładne książki z grami, a czasem trzeba było wpisywać wartości pod konkretne adresy. Mieli zdjęcie paczki pudełek, przyrostowo oznaczone 0, 1, 2 ... i wyjaśniono, że tylko jedna mała rzecz (bajt) może zmieścić się w tych pudełkach, a było ich dużo - niektóre komputery miał aż 65535! Byli obok siebie i wszyscy mieli adres.
Do ćwiczeń? Stwórz strukturę:
Taki sam przykład jak powyżej, z wyjątkiem C:
Wynik:
Być może to wyjaśnia niektóre podstawy poprzez przykład?
źródło
Powodem, dla którego miałem trudności ze zrozumieniem wskaźników, jest to, że wiele wyjaśnień zawiera wiele bzdur na temat przekazywania referencji. Wszystko to powoduje zamieszanie. Kiedy używasz parametru wskaźnika, nadal przekazujesz wartość; ale wartością jest raczej adres niż, powiedzmy, int.
Ktoś inny już powiązał ten samouczek, ale mogę podkreślić moment, w którym zacząłem rozumieć wskaźniki:
Samouczek dotyczący wskaźników i tablic w C: Rozdział 3 - Wskaźniki i łańcuchy
W chwili, gdy przeczytałem te słowa, chmury się rozdzieliły, a promień słońca ogarnął mnie zrozumieniem.
Nawet jeśli jesteś programistą VB .NET lub C # (tak jak ja) i nigdy nie używasz niebezpiecznego kodu, nadal warto zrozumieć, w jaki sposób działają wskaźniki, w przeciwnym razie nie zrozumiesz, w jaki sposób działają odwołania do obiektów. Wtedy będziesz miał powszechne, ale błędne przekonanie, że przekazanie odwołania do obiektu do metody kopiuje obiekt.
źródło
Uważam, że „Samouczek na temat wskaźników i tablic w C” Teda Jensena stanowi doskonałe źródło wiedzy na temat wskaźników. Jest podzielony na 10 lekcji, zaczynając od wyjaśnienia, jakie są wskaźniki (i do czego służą), a kończąc na wskaźnikach funkcji. http://home.netcom.com/~tjensen/ptr/cpoint.htm
Przechodząc od tego momentu, Przewodnik po programowaniu sieciowym Beej uczy API gniazd Unix, z którego możesz zacząć robić naprawdę fajne rzeczy. http://beej.us/guide/bgnet/
źródło
Złożoność wskaźników wykracza poza to, czego możemy łatwo nauczyć. Umożliwianie uczniom wzajemnego wskazywania siebie i używanie kartek z adresami domowymi to świetne narzędzia do nauki. Świetnie sobie radzą z wprowadzaniem podstawowych pojęć. Rzeczywiście, poznanie podstawowych pojęć jest niezbędne do skutecznego korzystania ze wskaźników. Jednak w kodzie produkcyjnym powszechne jest wchodzenie w znacznie bardziej złożone scenariusze, niż te proste demonstracje mogą zawierać w sobie.
Byłem zaangażowany w systemy, w których mieliśmy struktury wskazujące na inne struktury wskazujące na inne struktury. Niektóre z tych struktur zawierały również struktury osadzone (zamiast wskaźników do dodatkowych struktur). Tutaj wskaźniki stają się naprawdę mylące. Jeśli masz wiele poziomów pośrednich i zaczynasz z kodem takim jak ten:
może szybko się pomylić (wyobraź sobie dużo więcej linii i potencjalnie więcej poziomów). Wrzucaj tablice wskaźników i wskaźniki od węzła do węzła (drzewa, listy połączone) i robi się coraz gorzej. Widziałem, jak niektórzy naprawdę dobrzy programiści gubili się, gdy zaczęli pracować na takich systemach, nawet programiści, którzy naprawdę dobrze rozumieli podstawy.
Złożone struktury wskaźników również niekoniecznie wskazują na słabe kodowanie (choć mogą). Kompozycja jest istotnym elementem dobrego programowania obiektowego, a w językach z surowymi wskaźnikami nieuchronnie doprowadzi do wielowarstwowej pośredniości. Co więcej, systemy często muszą używać bibliotek stron trzecich ze strukturami, które nie pasują do siebie stylem ani techniką. W takich sytuacjach naturalnie pojawi się złożoność (choć z pewnością powinniśmy walczyć z nią jak najwięcej).
Myślę, że najlepszą rzeczą, jaką uczelnie mogą zrobić, aby pomóc uczniom w nauce wskaźników, jest użycie dobrych demonstracji w połączeniu z projektami, które wymagają użycia wskaźnika. Jeden trudny projekt zrobi więcej dla zrozumienia wskaźnika niż tysiąc demonstracji. Demonstracje mogą dać ci płytkie zrozumienie, ale aby głęboko zrozumieć wskaźniki, musisz naprawdę ich użyć.
źródło
Pomyślałem, że dodam do tej listy analogię, która okazała się bardzo pomocna przy wyjaśnianiu wskaźników (jeszcze w tym dniu) jako nauczyciel informatyki; po pierwsze:
Ustaw scenę :
Rozważmy parking z 3 miejscami, te miejsca są ponumerowane:
W pewnym sensie jest to jak lokalizacja w pamięci, są one sekwencyjne i ciągłe .. coś w rodzaju tablicy. W tej chwili nie ma w nich samochodów, więc jest jak pusta tablica (
parking_lot[3] = {0}
).Dodaj dane
Parking nigdy nie pozostaje pusty długo ... gdyby tak się stało, byłoby to bezcelowe i nikt by go nie zbudował. Powiedzmy, że w miarę upływu dnia na parkingu zapełniają się 3 samochody, niebieski samochód, czerwony samochód i zielony samochód:
Te samochody są tego samego typu (samochód), więc jeden sposób, aby myśleć o tym, że nasze samochody są jakieś dane (powiedzmy
int
), ale mają różne wartości (blue
,red
,green
, to może być kolorenum
)Wprowadź wskaźnik
Teraz, gdy zabiorę cię na ten parking i poproszę, abyś znalazł mi niebieski samochód, wyciągniesz jeden palec i użyjesz go, aby wskazać niebieski samochód w miejscu 1. To jak zabranie wskaźnika i przypisanie go do adresu pamięci (
int *finger = parking_lot
)Twój palec (wskaźnik) nie jest odpowiedzią na moje pytanie. Patrzenie na palec nic mi nie mówi, ale jeśli spojrzę w miejsce, w które wskazuje palec (dereferencja wskaźnika), mogę znaleźć samochód (dane), którego szukałem.
Ponowne przypisanie wskaźnika
Teraz mogę poprosić cię o znalezienie czerwonego samochodu, a ty możesz przekierować palec na nowy samochód. Teraz twój wskaźnik (taki sam jak poprzednio) pokazuje mi nowe dane (miejsce parkingowe, na którym można znaleźć czerwony samochód) tego samego typu (samochód).
Wskaźnik nie zmienił się fizycznie, to wciąż twój palec, zmieniły się tylko dane, które mi pokazywał. (adres „miejsca parkingowego”)
Podwójne wskaźniki (lub wskaźnik do wskaźnika)
Działa to również z więcej niż jednym wskaźnikiem. Mogę zapytać, gdzie jest wskaźnik, który wskazuje na czerwony samochód, a ty możesz użyć drugiej ręki i wskazać palcem pierwszy palec. (to jest jak
int **finger_two = &finger
)Teraz, jeśli chcę wiedzieć, gdzie jest niebieski samochód, mogę podążać w kierunku pierwszego palca do drugiego palca, do samochodu (dane).
Zwisający wskaźnik
Powiedzmy teraz, że czujesz się bardzo jak posąg i chcesz w nieskończoność trzymać rękę na czerwonym samochodzie. Co jeśli ten czerwony samochód odjedzie?
Wskaźnik wciąż wskazując, gdzie czerwony samochód był , ale już nie jest. Powiedzmy, że wjeżdża tam nowy samochód ... samochód pomarańczowy. Teraz, gdy znów zapytam: „gdzie jest czerwony samochód”, nadal wskazujecie tam, ale teraz się mylicie. To nie jest czerwony samochód, tylko pomarańczowy.
Wskaźnik arytmetyczny
Ok, więc nadal wskazujesz na drugie miejsce parkingowe (teraz zajęte przez samochód Orange)
Cóż, mam teraz nowe pytanie ... Chcę poznać kolor samochodu na następnym miejscu parkingowym. Możesz zobaczyć, że wskazujesz na miejsce 2, więc po prostu dodajesz 1 i wskazujesz na następne miejsce. (
finger+1
), ponieważ teraz chciałem wiedzieć, jakie dane tam są, musisz sprawdzić to miejsce (nie tylko palec), abyś mógł uszanować wskaźnik (*(finger+1)
), aby zobaczyć, że jest tam zielony samochód (dane w tej lokalizacji )źródło
"without getting them bogged down in the overall concept"
jako zrozumienie na wysokim poziomie. I do rzeczy :"I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"
- byłbyś bardzo zaskoczony, jak wiele osób nie rozumie wskaźników nawet do tego poziomuNie sądzę, że wskaźniki jako koncepcja są szczególnie trudne - modele mentalne większości uczniów odwzorowują coś takiego, a niektóre szybkie szkice pudełkowe mogą pomóc.
Trudność, przynajmniej ta, której doświadczyłem w przeszłości i widziałem, jak radzą sobie inni, polega na tym, że zarządzanie wskaźnikami w C / C ++ może być niepotrzebnie zawiłe.
źródło
Przykład samouczka z dobrym zestawem diagramów bardzo pomaga w zrozumieniu wskaźników .
Joel Spolsky mówi kilka dobrych rzeczy na temat zrozumienia wskaźników w swoim artykule Guerrilla Guide to Interviewing :
źródło
Problemem ze wskaźnikami nie jest koncepcja. To wykonanie i język. Dodatkowe zamieszanie wynika z tego, że nauczyciele zakładają, że trudna jest KONCEPCJA wskaźników, a nie żargon ani zawiły bałagan C i C ++ w tej koncepcji. Tak wiele wysiłku włożono w wyjaśnienie tej koncepcji (jak w zaakceptowanej odpowiedzi na to pytanie) i jest to po prostu marnowanie na kogoś takiego jak ja, ponieważ już to wszystko rozumiem. To tylko wyjaśnia niewłaściwą część problemu.
Aby dać ci wyobrażenie o tym, skąd pochodzę, jestem kimś, kto doskonale rozumie wskaźniki i mogę je umiejętnie używać w języku asemblera. Ponieważ w języku asemblera nie są one nazywane wskaźnikami. Są one nazywane adresami. Jeśli chodzi o programowanie i używanie wskaźników w C, popełniam wiele błędów i bardzo się mylę. Nadal tego nie załatwiłem. Dam ci przykład.
Gdy interfejs API mówi:
czego on chce
może chcieć:
liczba reprezentująca adres bufora
(Aby to powiedzieć
doIt(mybuffer)
, powiem , lubdoIt(*myBuffer)
?)liczba reprezentująca adres do adresu do bufora
(czy to
doIt(&mybuffer)
czydoIt(mybuffer)
czydoIt(*mybuffer)
?)liczba reprezentująca adres do adresu do adresu do bufora
(może to
doIt(&mybuffer)
. czy to jestdoIt(&&mybuffer)
? a nawetdoIt(&&&mybuffer)
)i tak dalej, a zastosowany język nie wyjaśnia tego tak wyraźnie, ponieważ obejmuje słowa „wskaźnik” i „odniesienie”, które nie mają dla mnie tyle znaczenia i przejrzystości, co „x trzyma adres y” i „ ta funkcja wymaga adresu do y ”. Odpowiedź zależy dodatkowo od tego, od czego, do cholery, należy „mybuffer” i od czego zamierza to zrobić. Język nie obsługuje poziomów zagnieżdżania spotykanych w praktyce. Na przykład, gdy muszę przekazać „wskaźnik” funkcji, która tworzy nowy bufor i modyfikuje wskaźnik, aby wskazywał na nowe położenie bufora. Czy naprawdę chce wskaźnika, czy wskaźnika do wskaźnika, więc wie, gdzie iść, aby zmodyfikować zawartość wskaźnika. Przez większość czasu muszę zgadywać, co oznacza „
„Wskaźnik” jest po prostu zbyt przeciążony. Czy wskaźnik jest adresem do wartości? czy jest to zmienna, która przechowuje adres wartości. Kiedy funkcja chce wskaźnika, czy potrzebuje adresu, który przechowuje zmienna wskaźnika, czy też chce adresu do zmiennej wskaźnika? Jestem zmieszany.
źródło
double *(*(*fn)(int))(char)
, wynikiem oceny*(*(*fn)(42))('x')
będziedouble
. Możesz usunąć warstwy oceny, aby zrozumieć, jakie muszą być typy pośrednie.(*(*fn)(42))('x')
?x
), gdzie, jeśli ocenisz*x
, dostajesz podwójną.fn
jest, a bardziej w kategoriach tego, co możesz zrobić zfn
Myślę, że główną barierą w zrozumieniu wskaźników są źli nauczyciele.
Niemal wszyscy uczą się kłamstw na temat wskaźników: że są niczym innym jak adresami pamięci lub że pozwalają ci wskazywać dowolne lokalizacje .
I oczywiście, że są trudne do zrozumienia, niebezpieczne i na wpół magiczne.
Nic z tego nie jest prawdą. Wskaźniki są w rzeczywistości dość prostymi pojęciami, o ile trzymasz się tego, co mówi o nich język C ++ i nie nasycasz ich atrybutami, które „zwykle” sprawdzają się w praktyce, ale język nie gwarantuje tego i nie są częścią rzeczywistej koncepcji wskaźnika.
Kilka miesięcy temu próbowałem napisać wyjaśnienie tego na blogu - mam nadzieję, że to komuś pomoże.
(Uwaga: zanim ktokolwiek się na mnie zwróci, tak, standard C ++ mówi o tym wskaźniki reprezentują adresy pamięci. Ale nie mówi, że „wskaźniki są adresami pamięci i tylko adresami pamięci i mogą być używane lub rozważane zamiennie z pamięcią adresy ". Rozróżnienie jest ważne)
źródło
Myślę, że to, co utrudnia uczenie się wskaźników, polega na tym, że dopóki wskaźniki nie są wygodne z pomysłem, że „w tym miejscu pamięci znajduje się zestaw bitów, które reprezentują liczbę całkowitą, podwójną, znak, cokolwiek”.
Kiedy pierwszy raz widzisz wskaźnik, tak naprawdę nie dostajesz tego, co jest w tym miejscu pamięci. „Co masz na myśli mówiąc, że ma adres ?”
Nie zgadzam się z opinią, że „albo je dostaniesz, albo nie”.
Stają się łatwiejsze do zrozumienia, gdy zaczniesz znajdować dla nich prawdziwe zastosowania (np. Nieprzekazywanie dużych struktur w funkcje).
źródło
Tak trudno zrozumieć, ponieważ nie jest to trudna koncepcja, ale dlatego, że składnia jest niespójna .
Najpierw dowiesz się, że skrajna lewa część tworzenia zmiennej określa jej typ. Deklaracja wskaźnika nie działa w ten sposób w C i C ++. Zamiast tego mówią, że zmienna wskazuje na typ po lewej stronie. W tym przypadku:
*
mypointer wskazuje na int.Nie w pełni zrozumiałem wskaźniki, dopóki nie spróbowałem ich użyć w języku C # (z niebezpiecznym), działają one dokładnie w ten sam sposób, ale z logiczną i spójną składnią. Wskaźnik jest samym typem. Tutaj mypointer jest wskaźnikiem do int.
Nawet nie zaczynaj mi wskaźników funkcji ...
źródło
int *p;
ma proste znaczenie:*p
jest liczbą całkowitą.int *p, **pp
oznacza:*p
i**pp
są liczbami całkowitymi.*p
i**pp
to nie całkowite, bo nigdy nie zainicjowanyp
lubpp
lub*pp
do punktu do niczego. Rozumiem, dlaczego niektórzy wolą trzymać się gramatyki na tym, szczególnie dlatego, że niektóre przypadki brzegowe i złożone wymagają tego (chociaż nadal możesz trywialnie obejść to we wszystkich przypadkach, o których wiem) ... ale nie sądzę, aby te przypadki były ważniejsze niż fakt, że nauczanie prawidłowego wyrównywania wprowadza w błąd początkujących. Nie wspominając o brzydkim stylu! :)Mógłbym pracować ze wskaźnikami, kiedy znałem tylko C ++. W pewnym stopniu wiedziałem, co robić w niektórych przypadkach, a czego nie robić od próby / błędu. Ale rzeczą, która dała mi pełne zrozumienie, jest język asemblera. Jeśli wykonujesz poważne debugowanie na poziomie instrukcji za pomocą napisanego programu w asemblerze, powinieneś być w stanie zrozumieć wiele rzeczy.
źródło
Lubię analogię adresu domowego, ale zawsze myślałem o adresie do samej skrzynki pocztowej. W ten sposób możesz wizualizować koncepcję dereferencji wskaźnika (otwieranie skrzynki pocztowej).
Na przykład postępując zgodnie z połączoną listą: 1) zacznij od swojego papieru z adresem 2) Idź do adresu na papierze 3) Otwórz skrzynkę pocztową, aby znaleźć nową kartkę papieru z następnym adresem
Na połączonej liniowo liście w ostatniej skrzynce nie ma nic (koniec listy). Na okrągłej połączonej liście ostatnia skrzynka pocztowa zawiera adres pierwszej skrzynki pocztowej.
Pamiętaj, że krok 3 to miejsce, w którym występuje odwołanie i gdzie się zawiesisz lub pomylisz, gdy adres jest nieprawidłowy. Zakładając, że możesz podejść do skrzynki pocztowej niepoprawnego adresu, wyobraź sobie, że jest tam czarna dziura lub coś, co wywraca świat na lewą stronę :)
źródło
Myślę, że głównym powodem, dla którego ludzie mają z tym problem, jest to, że na ogół nie uczy się go w interesujący i angażujący sposób. Chciałbym zobaczyć, jak wykładowca bierze 10 ochotników z tłumu i daje im 1 metr linijki, każe im stać w pewnej konfiguracji i używać władców, aby wskazywać na siebie. Następnie pokaż arytmetykę wskaźnika, przesuwając ludzi wokół (i gdzie wskazują swoich władców). Byłby to prosty, ale skuteczny (a przede wszystkim niezapomniany) sposób pokazania koncepcji bez zbytniego zagłębiania się w mechanikę.
Po przejściu do C i C ++ wydaje się, że dla niektórych osób staje się to trudniejsze. Nie jestem pewien, czy dzieje się tak dlatego, że w końcu wprowadzają teorię, której nie rozumieją właściwie w praktyce, lub dlatego, że manipulowanie wskaźnikiem jest z natury trudniejsze w tych językach. Tak dobrze nie pamiętam własnego przejścia, ale znałem wskaźniki w Pascalu, a następnie przeniosłem się do C i całkowicie się zgubiłem.
źródło
Nie sądzę, że same wskaźniki są mylące. Większość ludzi może zrozumieć tę koncepcję. Teraz o ilu wskaźnikach możesz pomyśleć lub o ilu poziomach pośrednich czujesz się swobodnie. Nie potrzeba zbyt wielu, aby postawić ludzi na krawędzi. Fakt, że mogą zostać przypadkowo zmienione przez błędy w twoim programie, może również bardzo utrudnić debugowanie, gdy coś pójdzie nie tak w twoim kodzie.
źródło
Myślę, że może to być problem ze składnią. Składnia C / C ++ dla wskaźników wydaje się niespójna i bardziej złożona, niż powinna być.
Jak na ironię rzeczą, która naprawdę pomogła mi zrozumieć wskaźniki, była koncepcja iteratora w standardowej bibliotece szablonów c ++ . To ironiczne, ponieważ mogę jedynie założyć, że iteratory zostały pomyślane jako uogólnienie wskaźnika.
Czasami po prostu nie widzisz lasu, dopóki nie nauczysz się ignorować drzewa.
źródło
(*p)
tak było(p->)
, i dlatego mielibyśmyp->->x
zamiast niejasności*p->x
a->b
po prostu znaczy(*a).b
.* p->x
sposób* ((*a).b)
podczas gdy*p -> x
środki(*(*p)) -> x
. Mieszanie operatorów prefiksów i postfiksów powoduje niejednoznaczne analizowanie.1+2 * 3
powinno być 9.Zamieszanie wynika z wielu warstw abstrakcji zmieszanych razem w koncepcji „wskaźnika”. Programiści nie są zdezorientowani zwykłymi odniesieniami w Javie / Pythonie, ale wskaźniki różnią się tym, że ujawniają cechy podstawowej architektury pamięci.
Dobrą zasadą jest czyste oddzielanie warstw abstrakcji, a wskaźniki tego nie robią.
źródło
foo[i]
oznacza dotarcie do określonego miejsca, przejście do przodu o określoną odległość i zobaczenie, co tam jest. To, co komplikuje sprawy, to znacznie bardziej skomplikowana dodatkowa warstwa abstrakcji, która została dodana przez Standard wyłącznie na korzyść kompilatora, ale modeluje rzeczy w sposób, który jest nieodpowiedni dla potrzeb programisty i kompilatora.Sposób, w jaki lubiłem to wyjaśniać, to tablice i indeksy - ludzie mogą nie znać wskaźników, ale ogólnie wiedzą, czym jest indeks.
Mówię więc, wyobraź sobie, że pamięć RAM jest tablicą (a masz tylko 10 bajtów pamięci RAM):
Następnie wskaźnik do zmiennej jest tak naprawdę tylko indeksem (pierwszy bajt) tej zmiennej w pamięci RAM.
Więc jeśli masz wskaźnik / indeks
unsigned char index = 2
, wówczas wartość jest oczywiście trzecim elementem lub liczbą 4. Wskaźnik do wskaźnika to miejsce, w którym bierzesz tę liczbę i używasz jej jako samego indeksuRAM[RAM[index]]
.Narysowałbym tablicę na liście papieru i po prostu użyłem jej, aby pokazać takie rzeczy, jak wiele wskaźników wskazujących na tę samą pamięć, arytmetykę wskaźnika, wskaźnik na wskaźnik i tak dalej.
źródło
Numer skrytki pocztowej.
Jest to informacja, która pozwala uzyskać dostęp do czegoś innego.
(A jeśli wykonujesz arytmetykę na numerach skrytek pocztowych, możesz mieć problem, ponieważ list trafia do niewłaściwej skrzynki. A jeśli ktoś przejdzie do innego stanu - bez adresu do przekazania - masz zwisający wskaźnik. z drugiej strony - jeśli poczta przesyła pocztę, oznacza to, że masz wskaźnik do wskaźnika).
źródło
Nie jest to zły sposób na uchwycenie go za pomocą iteratorów ... ale patrz dalej, zobaczysz, jak Alexandrescu zaczyna narzekać na nie.
Wielu byłych programistów C ++ (którzy nigdy nie rozumieli, że iteratory są nowoczesnym wskaźnikiem przed zrzuceniem języka) skaczą do C # i nadal uważają, że mają przyzwoite iteratory.
Hmm, problem polega na tym, że wszystkie iteratory są całkowicie w sprzeczności z tym, co platformy uruchomieniowe (Java / CLR) starają się osiągnąć: nowe, proste, uniwersalne zastosowanie dla każdego. Co może być dobre, ale powiedzieli to raz w fioletowej książce i powiedzieli to nawet przed i przed C:
Pośrednictwo
Bardzo potężna koncepcja, ale nigdy tak nie jest, jeśli robisz to do końca. Iteratory są użyteczne, ponieważ pomagają w abstrakcji algorytmów, inny przykład. A czas kompilacji to miejsce na algorytm, bardzo proste. Znasz kod + dane lub w tym innym języku C #:
IEnumerable + LINQ + Massive Framework = 300 MB pośrednia kara w czasie wykonywania kiepskich, przeciągających aplikacje przez sterty instancji typów referencyjnych.
„Le Pointer jest tani”.
źródło
Niektóre powyższe odpowiedzi potwierdziły, że „wskaźniki nie są naprawdę trudne”, ale nie odniosły się bezpośrednio do tego, gdzie „wskaźnik jest trudny!” pochodzi z. Kilka lat temu uczyłem studentów pierwszego roku CS (tylko przez jeden rok, ponieważ najwyraźniej byłem do niczego) i było dla mnie jasne, że idea wskaźnika nie jest trudna. Trudne jest zrozumienie, dlaczego i kiedy chcesz wskaźnik .
Nie sądzę, żebyś mógł rozwieść to pytanie - dlaczego i kiedy używać wskaźnika - od wyjaśniania szerszych problemów inżynierii oprogramowania. Dlaczego każda zmienna nie powinna być zmienną globalną i dlaczego należy wyodrębnić podobny kod w funkcjach (że, weź to, użyj wskaźników, aby specjalizować swoje zachowanie na stronie wywołań).
źródło
Nie rozumiem, co jest takiego mylącego w przypadku wskaźników. Wskazują miejsce w pamięci, to znaczy, że przechowuje adres pamięci. W C / C ++ możesz określić typ, na który wskazuje wskaźnik. Na przykład:
Mówi, że mój_int_pointer zawiera adres do lokalizacji, która zawiera int.
Problem ze wskaźnikami polega na tym, że wskazują one lokalizację w pamięci, więc łatwo jest odszukać miejsce, w którym nie powinieneś być. Jako dowód przyjrzyj się licznym lukom bezpieczeństwa w aplikacjach C / C ++ przed przepełnieniem bufora (zwiększanie wskaźnika poza wyznaczoną granicą).
źródło
Żeby trochę zamieszać, czasem trzeba pracować z uchwytami zamiast wskaźników. Uchwyty są wskaźnikami do wskaźników, dzięki czemu zaplecze może przenosić rzeczy w pamięci w celu defragmentacji sterty. Jeśli wskaźnik zmieni się w połowie rutyny, wyniki są nieprzewidywalne, więc najpierw musisz zablokować uchwyt, aby upewnić się, że nic nie pójdzie.
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 mówi o tym nieco bardziej spójnie niż ja. :-)
źródło
Każdy początkujący w C / C ++ ma ten sam problem i ten problem występuje nie dlatego, że „wskaźników trudno się nauczyć”, ale „kogo i jak to wyjaśniono”. Niektórzy uczniowie zbierają to werbalnie, a niektórzy wizualnie, a najlepszym sposobem wyjaśnienia jest użycie przykładu „trenowania” (w przypadku werbalnego i wizualnego).
Gdzie „lokomotywa” to wskaźnik, który niczego nie może utrzymać, a „wagon” jest tym, co „lokomotywa” próbuje pociągnąć (lub wskazać). Następnie możesz sklasyfikować sam „wagon”, czy może on pomieścić zwierzęta, rośliny lub ludzi (lub ich mieszankę).
źródło