Jakie są bariery w zrozumieniu wskaźników i co można zrobić, aby je pokonać? [Zamknięte]

449

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.

David McGraw
źródło
20
Teza tego pytania jest taka, że ​​wskaźniki są trudne do zrozumienia. Pytanie nie daje dowodów na to, że wskaźniki są trudniejsze do zrozumienia niż cokolwiek innego.
bmargulies
14
Może coś mi brakuje (ponieważ koduję w językach GCC), ale zawsze myślałem, czy wskaźniki w pamięci są strukturą Key-> Value. Ponieważ przekazywanie dużych ilości danych w programie jest drogie, tworzysz strukturę (wartość) i przekazujesz wskaźnik / referencję (klucz), ponieważ klucz jest znacznie mniejszą reprezentacją większej struktury. Trudność polega na tym, że musisz porównać dwa wskaźniki / referencje (czy porównujesz klucze lub wartości), co wymaga więcej pracy, aby włamać się do danych zawartych w strukturze (wartości).
Evan Plaice
2
@ Wolfpack'08 „Wydaje mi się, że pamięć w adresie będzie zawsze int.” - W takim razie powinno ci się wydawać, że nic nie ma typu, ponieważ wszystkie są tylko bitami w pamięci. „W rzeczywistości typ wskaźnika jest rodzajem zmiennej, na którą wskazuje wskaźnik” - Nie, typ wskaźnika jest wskaźnikiem typu zmiennej, na którą wskazuje wskaźnik - co jest naturalne i powinno być oczywiste.
Jim Balter,
2
Zawsze zastanawiałem się, co tak trudno zrozumieć, że zmienne (i funkcje) to tylko bloki pamięci, a wskaźniki to zmienne przechowujące adresy pamięci. Ten być może zbyt praktyczny model myślowy może nie zaimponować wszystkim fanom abstrakcyjnych koncepcji, ale doskonale pomaga zrozumieć, w jaki sposób działają wskaźniki.
Christian Rau,
8
W skrócie, uczniowie prawdopodobnie nie rozumieją, ponieważ nie rozumieją poprawnie, lub wcale, jak ogólnie działa pamięć komputera, a konkretnie „model pamięci” w języku C. Ta książka Programowanie od podstaw daje bardzo dobrą lekcję na te tematy.
Abbafei

Odpowiedzi:

745

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 hpo 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ść hzmiennej 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ż h1został wyczyszczony, h2nadal 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ł NextHouseodniesienia, 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):

  • h1 = 4
  • h2 = 23

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.

Lasse V. Karlsen
źródło
59
Przepełnienie bufora jest przezabawne. „Sąsiad wraca do domu, roztrzaskuje czaszkę i wsuwa się w twoje śmieci, i pozywa cię w zapomnienie”.
gtd
12
To jasne wyjaśnienie tej koncepcji. Ta koncepcja NIE jest jednak myląca dla wskaźników, więc cały ten esej był trochę zmarnowany.
Breton
10
Ale tylko prosić, czego Ci znaleźć mylące o wskaźniki?
Lasse V. Karlsen
11
Odwiedziłem ten post kilka razy, odkąd napisałeś odpowiedź. Twoje wyjaśnienia dotyczące kodu są doskonałe i doceniam to, że ponownie go odwiedzasz, aby dodać / dopracować więcej myśli. Bravo Lasse!
David McGraw
3
Nie ma mowy, żeby jedna strona tekstu (bez względu na to, jak długo) mogła podsumować każdy niuans zarządzania pamięcią, referencji, wskaźników itp. Ponieważ głosowało 465 osób, powiedziałbym, że jest wystarczająco dobry strona początkowa informacji. Czy jest coś więcej do nauczenia się? Jasne, kiedy nie jest?
Lasse V. Karlsen
153

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.

Tryke
źródło
5
Nie sądzę, że się pomyliłem. Moim zamiarem była zmiana wartości zmiennej wskazanej na John z Sama. Trochę trudniej jest reprezentować ludzi, ponieważ wygląda na to, że zmieniasz wartość obu wskaźników.
Tryke,
24
Jednak powodem tego jest zamieszanie, ponieważ John nie wstał z miejsca, a potem Sam usiadł, jak możemy sobie wyobrazić. Bardziej przypomina to, że Sam podszedł i włożył rękę do Johna i sklonował programowanie Sama do ciała Johna, jak Hugo tkający w matrycy ponownie załadowanej.
Breton
59
Bardziej jak Sam zajmuje miejsce Johna, a John unosi się po pokoju, dopóki nie wpada na coś krytycznego i nie powoduje awarii.
just_wes
2
Dla mnie ten przykład jest niepotrzebnie skomplikowany. Mój prof powiedział mi, żebym wskazał na światło, i powiedział: „Twoja ręka jest wskaźnikiem do obiektu świetlnego”.
Celeritas
Problem z tego typu przykładami polega na tym, że wskaźnik do X i X nie jest taki sam. I to nie jest przedstawiane ludziom.
Isaac Nequittepas,
124

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ę.

Wilka
źródło
15
Naprawdę to lubię. Nietrudno zauważyć, że dwukrotne napisanie hiperłącza nie powoduje pojawienia się dwóch stron internetowych (tak jak int *a = bnie tworzy dwóch kopii *b).
detly
4
Jest to w rzeczywistości bardzo intuicyjne i każdy powinien móc się z nim odnosić. Chociaż istnieje wiele scenariuszy, w których ta analogia się rozpada. Świetne na szybkie wprowadzenie. +1
Brian Wigginton
Łącze do dwukrotnie otwieranej strony zwykle tworzy dwa prawie całkowicie niezależne wystąpienia tej strony. Myślę, że hiperłącze może być dobrą analogią do konstruktora, ale nie do wskaźnika.
Utkan Gezer
@ThoAppelsin Niekoniecznie prawda, jeśli na przykład uzyskujesz dostęp do statycznej strony HTML, masz dostęp do pojedynczego pliku na serwerze.
dramatyczny
5
Przemyślasz to. Hiperłącza wskazują na pliki na serwerze, to jest zakres analogii.
dramatyczny
48

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ą.

JSN
źródło
2
Ciekawy. Nie mam pojęcia, jak zacząć o tym mówić. Jakieś zasoby do udostępnienia?
Karolis
1
Zgadzam się. Na przykład nauczyłem się programować w asemblerze przed C i wiedząc, jak działają rejestry, nauka wskaźników była łatwa. W rzeczywistości niewiele się nauczyło, wszystko przyszło bardzo naturalnie.
Milan Babuškov
Weź podstawowy procesor, powiedz coś, co uruchamia kosiarki lub zmywarki do naczyń i zaimplementuj to. Lub bardzo bardzo podstawowy podzbiór ARM lub MIPS. Oba mają bardzo prosty ISA.
Daniel Goldberg,
1
Warto zauważyć, że takie podejście edukacyjne zostało popierane / praktykowane przez samego Donalda Knutha. Sztuka programowania komputerowego Knutha opisuje prostą hipotetyczną architekturę i prosi uczniów o wdrożenie rozwiązań w celu ćwiczenia problemów w hipotetycznym języku asemblera dla tej architektury. Gdy stało się to praktycznie wykonalne, niektórzy uczniowie czytający książki Knutha faktycznie wdrożyli jego architekturę jako maszynę wirtualną (lub wykorzystali istniejącą implementację) i faktycznie uruchomili swoje rozwiązania. IMO to świetny sposób na naukę, jeśli masz czas.
WeirdlyCheezy
4
@Luke Nie sądzę, że tak łatwo jest zrozumieć ludzi, którzy po prostu nie potrafią zrozumieć wskaźników (a ściślej mówiąc, pośredniość w ogóle). Zasadniczo zakładasz, że ludzie, którzy nie rozumieją wskaźników w C, będą mogli rozpocząć naukę asemblera, zrozumieć podstawową architekturę komputera i powrócić do C ze zrozumieniem wskaźników. Może to być prawdą dla wielu, ale według niektórych badań wydaje się, że niektórzy ludzie z natury nie potrafią zrozumieć pośrednictwa, nawet w zasadzie (nadal trudno mi w to uwierzyć, ale być może po prostu miałem szczęście z moimi „uczniami” „).
Luaan
27

Dlaczego wskaźniki są tak wiodącym czynnikiem dezorientacji wielu nowych, a nawet starszych studentów szkół wyższych w języku C / C ++?

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 .

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ą?

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.

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.

Do ćwiczeń? Stwórz strukturę:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Taki sam przykład jak powyżej, z wyjątkiem C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Wynik:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

Być może to wyjaśnia niektóre podstawy poprzez przykład?

Josh
źródło
+1 za „bez zrozumienia, jak fizycznie układana jest pamięć”. Przybyłem do C z języka asemblera, a koncepcja wskaźników była bardzo naturalna i łatwa; i widziałem ludzi z wyższym poziomem znajomości języka, którzy próbują to rozgryźć. Co gorsza, składnia jest myląca (wskaźniki funkcji!), Więc uczenie się pojęcia i składni w tym samym czasie jest receptą na kłopoty.
Brendan
4
Wiem, że to stary post, ale byłoby wspaniale, gdyby wyjście dostarczonego kodu zostało dodane do posta.
Josh
Tak, jest podobny do algebry (chociaż algebry mają dodatkowy punkt zrozumiałości, ponieważ ich „zmienne” są niezmienne). Ale około połowa ludzi, których znam, nie zna algebry w praktyce. Po prostu się dla nich nie oblicza. Znają wszystkie „równania” i recepty, aby dojść do wyniku, ale stosują je nieco przypadkowo i niezgrabnie. I nie mogą rozszerzyć ich do własnych celów - to po prostu jakaś niezmienna, nieskomponowana czarna skrzynka dla nich. Jeśli rozumiesz algebrę i potrafisz z niej efektywnie korzystać, jesteś już daleko przed paczką - nawet wśród programistów.
Luaan
24

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

int puts(const char *s);

Na razie zignoruj const.parametr Przekazywany parametr puts()to wskaźnik, czyli wartość wskaźnika (ponieważ wszystkie parametry w C są przekazywane przez wartość), a wartość wskaźnika to adres, na który wskazuje, lub po prostu , adres. Kiedy więc piszemy, puts(strA);jak widzieliśmy, przekazujemy adres strA [0].

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.

Kyralessa
źródło
Pozostawia mnie zastanawianie się, jaki jest sens posiadania wskaźnika. W większości napotkanych bloków kodu wskaźnik jest widoczny tylko w deklaracji.
Wolfpack'08,
@ Wolfpack'08 ... co ?? Na jaki kod patrzysz?
Kyle Strand
@KyleStrand Pozwól mi rzucić okiem.
Wolfpack'08,
19

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/

Ted Percival
źródło
1
Drugi samouczek Teda Jensena. Rozkłada wskaźniki do poziomu szczegółowości, który nie jest zbyt szczegółowy, nie ma żadnej książki, którą czytałem. Niezwykle pomocny! :)
Dave Gallagher,
12

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:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

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ć.

Derek Park
źródło
10

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:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

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:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

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ć kolor enum)


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?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

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)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

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 )

Mikrofon
źródło
Po prostu nie używaj słowa „podwójny wskaźnik”. Wskaźniki mogą wskazywać na wszystko, więc oczywiście możesz mieć wskaźniki wskazujące na inne wskaźniki. Nie są podwójnymi wskaźnikami.
gnasher729
Myślę, że to pomija punkt, w którym same „palce”, aby kontynuować swoją analogię, „zajmują miejsce parkingowe”. Nie jestem pewien, czy ludzie mają trudności ze zrozumieniem wskaźników na wysokim poziomie abstrakcji twojej analogii, rozumie, że wskaźniki są zmiennymi rzeczami, które zajmują miejsca w pamięci, i jak to jest użyteczne, wydaje się, że omija ludzi.
Emmet
1
@Emmet - nie zgadzam się, że istnieje wiele innych wskaźników WRT, ale czytam pytanie: "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 poziomu
Mike
Czy istnieje sens rozszerzenia analogii samochód-palec na osobę (jednym lub więcej palcami - i nieprawidłowości genetyczne, które mogą pozwolić każdemu z nich wskazywać w dowolnym kierunku!), Usiadł w jednym z samochodów wskazujących na inny samochód (lub pochylił się nad wskazaniem pustkowia obok działki jako „niezainicjowany wskaźnik” lub cała ręka rozłożyła się wskazując na rząd odstępów jako „tablica wskaźników o stałym rozmiarze [5]” lub zwinęła się w dłoni „wskaźnik zerowy” wskazuje to na miejsce, w którym wiadomo, że NIGDY nie ma samochodu) ... 8-)
SlySven
twoje wyjaśnienie było znaczące i dobre dla początkującego.
Yatendra Rathore
9

Nie 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.

Matt Mattchell
źródło
8

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:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

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)czy doIt(mybuffer)czydoIt(*mybuffer) ?)

liczba reprezentująca adres do adresu do adresu do bufora

(może to doIt(&mybuffer). czy to jest doIt(&&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.

Breton
źródło
Widziałem to wyjaśnione w ten sposób: jeśli zobaczysz deklarację wskaźnika podobną double *(*(*fn)(int))(char), wynikiem oceny *(*(*fn)(42))('x')będzie double. Możesz usunąć warstwy oceny, aby zrozumieć, jakie muszą być typy pośrednie.
Bernd Jendrissek
@BerndJendrissek Nie jestem pewien, czy śledzę. Jaki jest zatem wynik oceny (*(*fn)(42))('x') ?
Breton,
dostajesz rzecz (nazwijmy to x), gdzie, jeśli ocenisz *x, dostajesz podwójną.
Bernd Jendrissek,
@BerndJendrissek Czy to ma wyjaśniać coś na temat wskaźników? Nie rozumiem O co ci chodzi? Zdjąłem warstwę i nie uzyskałem żadnych nowych informacji o żadnych typach pośrednich. Co to tłumaczy na temat tego, jaką konkretną funkcję przyjmie? Co to ma z tym wspólnego?
Breton,
Być może przesłaniem w tym wyjaśnieniu (i nie jest moje, chciałbym znaleźć miejsce, w którym po raz pierwszy go zobaczyłem) jest myślenie mniej w kategoriach tego, co fn jest, a bardziej w kategoriach tego, co możesz zrobić zfn
Bernd Jendrissek
8

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)

jalf
źródło
W końcu wskaźnik zerowy nie wskazuje na zero adresu w pamięci, nawet jeśli jego „wartość” C wynosi zero. Jest to całkowicie osobna koncepcja, a jeśli źle sobie z tym poradzisz, możesz zająć się (i dereferencją) czymś, czego się nie spodziewałeś. W niektórych przypadkach może to być nawet zerowy adres w pamięci (szczególnie teraz, gdy przestrzeń adresowa jest zwykle płaska), ale w innych może zostać pominięty jako niezdefiniowane zachowanie przez kompilator optymalizujący lub uzyskać dostęp do innej powiązanej części pamięci z „zero” dla danego typu wskaźnika. Następuje wesołość.
Luaan
Niekoniecznie. Musisz mieć możliwość wymodelowania komputera w głowie, aby wskaźniki miały sens (a także debugowania innych programów). Nie każdy może to zrobić.
Thorbjørn Ravn Andersen
5

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).

Baltimark
źródło
5

Tak trudno zrozumieć, ponieważ nie jest to trudna koncepcja, ale dlatego, że składnia jest niespójna .

   int *mypointer;

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.

  int* mypointer;

Nawet nie zaczynaj mi wskaźników funkcji ...

burrrr
źródło
2
Właściwie oba wasze fragmenty są ważne C. To jest kwestia wielu lat stylu C, że pierwszy jest bardziej powszechny. Drugi jest na przykład o wiele bardziej powszechny w C ++.
RBerteig 18.04.2009
1
Drugi fragment nie działa zbyt dobrze w przypadku bardziej złożonych deklaracji. A składnia nie jest tak „niespójna”, gdy uświadomisz sobie, że prawa część deklaracji wskaźnika pokazuje, co musisz zrobić ze wskaźnikiem, aby uzyskać coś, czego typem jest specyfikator typu atomowego po lewej stronie.
Bernd Jendrissek
2
int *p;ma proste znaczenie: *pjest liczbą całkowitą. int *p, **ppoznacza: *pi **ppsą liczbami całkowitymi.
Miles Rout
@MilesRout: Ale to jest właśnie problem. *pi **ppto nie całkowite, bo nigdy nie zainicjowany plub pplub *ppdo 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! :)
Wyścigi lekkości na orbicie
@LightnessRacesinOrbit Nauczanie wyrównania do daleka nie jest mylące. Jest to jedyny właściwy sposób nauczania. NIE nauczanie tego jest mylące.
Miles Rout
5

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.

Toto
źródło
4

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ę :)

Christopher Scott
źródło
Paskudna komplikacja z analogią liczby skrzynek pocztowych polega na tym, że chociaż język wymyślony przez Dennisa Ritchiego definiuje zachowanie w kategoriach adresów bajtów i wartości przechowywanych w tych bajtach, język zdefiniowany w standardzie C zachęca do „optymalizowania” implementacji do użycia behawioralnego model, który jest bardziej skomplikowany, ale definiuje różne aspekty modelu w sposób niejednoznaczny, sprzeczny i niekompletny.
supercat
3

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.

Wolfbyte
źródło
2

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.

bruceatk
źródło
2

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.

Waylon Flinn
źródło
Problem dotyczy głównie składni deklaracji C. Ale użycie wskaźnika z pewnością byłoby łatwiejsze, gdyby (*p)tak było (p->), i dlatego mielibyśmy p->->xzamiast niejasności*p->x
MSalters
@MSalters O mój boże żartujesz, prawda? Nie ma tam niespójności. a->bpo prostu znaczy (*a).b.
Miles Rout
@Miles: Rzeczywiście, a przez to logiki * p->xsposób * ((*a).b)podczas gdy *p -> xśrodki (*(*p)) -> x. Mieszanie operatorów prefiksów i postfiksów powoduje niejednoznaczne analizowanie.
MSalters
@MSalters nie, ponieważ białe znaki nie mają znaczenia. To tak, jakby powiedzieć, że 1+2 * 3powinno być 9.
Miles Rout
2

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ą.

Joshua Fox
źródło
1
Interesujące jest to, że wskaźniki C w rzeczywistości nie ujawniają żadnych cech charakterystycznych dla podstawowej architektury pamięci. Jedyne różnice między odniesieniami Java i wskaźnikami C polegają na tym, że możesz mieć złożone typy zawierające wskaźniki (np. Int *** lub char * ( ) (void * )), istnieje arytmetyka wskaźników dla tablic i wskaźników do struktury elementów, obecność pustki * i dualność tablica / wskaźnik. Poza tym działają tak samo.
jpalecek
Słuszna uwaga. To arytmetyka wskaźnika i możliwość przepełnienia bufora - wyrwanie się z abstrakcji poprzez wyrwanie się z aktualnie odpowiedniego obszaru pamięci - to robi.
Joshua Fox
@jpalecek: Łatwo jest zrozumieć, w jaki sposób wskaźniki działają na implementacjach, które dokumentują ich zachowanie pod względem architektury bazowej. Mówienie 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.
supercat,
2

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):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

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 indeksu RAM[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.

sashoalm
źródło
1

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).

joel.neely
źródło
1

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”.

rama-jka toti
źródło
4
Co to ma do rzeczy?
Neil Williams
... co próbujesz powiedzieć, oprócz „statycznego łączenia jest najlepszą rzeczą na świecie” i „nie rozumiem, jak działa coś innego niż to, czego się nauczyłem wcześniej”?
Luaan
Luaan, nie mogłeś wiedzieć, czego można się nauczyć, demontując JIT w 2000 roku, prawda? To, że kończy się na stole do skoków, z tabeli ze wskazówkami, jak pokazano w 2000 roku w trybie online w ASM, więc niezrozumienie czegoś innego może mieć inne znaczenie: uważne czytanie jest podstawową umiejętnością, spróbuj ponownie.
rama-jka toti
1

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ń).

Bernd Jendrissek
źródło
0

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:

int* my_int_pointer;

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ą).

grom
źródło
0

Ż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. :-)

SarekOfVulcan
źródło
-1: Uchwyty nie są wskaźnikami do wskaźników; w żadnym sensie nie są wskaźnikami. Nie pomyl ich.
Ian Goldby,
„W żadnym sensie nie są wskaźnikami” - błagam, by się różnić.
SarekOfVulcan
Wskaźnik to miejsce w pamięci. Uchwyt to dowolny unikalny identyfikator. Może to być wskaźnik, ale równie dobrze może to być indeks w tablicy lub cokolwiek innego. Podany link jest tylko specjalnym przypadkiem, w którym uchwyt jest wskaźnikiem, ale nie musi tak być. Zobacz także parashift.com/c++-faq-lite/references.html#faq-8.8
Ian Goldby
Ten link nie potwierdza również twojego twierdzenia, że ​​nie są wskaźnikami w żadnym sensie - „Na przykład, uchwytami może być Fred **, gdzie wskazane wskaźniki Fred *…” Nie sądzę -1 było sprawiedliwe.
SarekOfVulcan
0

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ę).

Praveen Kumar
źródło