Jak wyjaśnić początkującym wskaźniki C (deklaracja vs. operatory jednoargumentowe)?


141

Niedawno miałem przyjemność wyjaśnić wskazówki początkującym programistom w C i napotkałem następującą trudność. Może się to w ogóle nie wydawać problemem, jeśli już wiesz, jak używać wskaźników, ale spróbuj spojrzeć na poniższy przykład z czystym umysłem:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

Dla absolutnie początkującego rezultat może być zaskakujący. W linii 2 właśnie zadeklarował * bar jako & foo, ale w linii 4 okazuje się, że * bar to w rzeczywistości foo zamiast & foo!

Można powiedzieć, że zamieszanie wynika z niejednoznaczności symbolu *: w linii 2 jest używany do zadeklarowania wskaźnika. W linii 4 jest używany jako operator jednoargumentowy, który pobiera wartość wskazywaną przez wskaźnik. Dwie różne rzeczy, prawda?

Jednak to „wyjaśnienie” wcale nie pomaga początkującym. Wprowadza nową koncepcję, wskazując na subtelną rozbieżność. To nie może być właściwy sposób nauczania.

Jak więc Kernighan i Ritchie to wyjaśnili?

Operator jednoargumentowy * jest operatorem pośrednim lub wyłuskiwaniem; po zastosowaniu do wskaźnika uzyskuje dostęp do obiektu wskazywanego przez wskaźnik. […]

Deklaracja wskaźnika ip int *ipjest traktowana jako mnemonik; mówi, że wyrażenie *ipjest int. Składnia deklaracji zmiennej naśladuje składnię wyrażeń, w których zmienna może się pojawić .

int *ippowinno być odczytywane jako „ *ipzwróci int”? Ale dlaczego w takim razie przypisanie po deklaracji nie jest zgodne z tym wzorcem? Co jeśli początkujący chce zainicjować zmienną? int *ip = 1(czytaj: *ipzwróci wartość inti intjest 1) nie będzie działać zgodnie z oczekiwaniami. Model konceptualny po prostu nie wydaje się spójny. Czy coś mi umyka?


Edycja: próbowano podsumować odpowiedzi tutaj .


15
Najlepszym wyjaśnieniem jest narysowanie rzeczy na papierze i połączenie ich strzałkami;)
Maroun

16
Kiedy musiałem wyjaśnić składnię wskaźników, zawsze nalegałem na fakt, że *w deklaracji jest to token oznaczający „deklaruj wskaźnik”, w wyrażeniach jest to operator wyłuskiwania i te dwa reprezentują różne rzeczy, które mają ten sam symbol (to samo co operator mnożenia - ten sam symbol, inne znaczenie). To zagmatwane, ale wszystko inne niż rzeczywisty stan rzeczy będzie jeszcze gorsze.
Matteo Italia

40
może pisząc to tak, by int* barbyło bardziej oczywiste, że gwiazda jest w rzeczywistości częścią typu, a nie częścią identyfikatora. Oczywiście powoduje to różne problemy z nieintuicyjnymi rzeczami, takimi jak int* a, b.
Niklas B.

9
Zawsze uważałem, że wyjaśnienie K&R jest głupie i niepotrzebne. Język używa tego samego symbolu dla dwóch różnych rzeczy i po prostu musimy sobie z tym poradzić. *może mieć dwa różne znaczenia w zależności od kontekstu. Tak jak ta sama litera może być wymawiana różnie w zależności od słowa, w którym jest, przez co trudno jest nauczyć się mówić wieloma językami. Gdyby każda koncepcja / operacja miała swój własny symbol, potrzebowalibyśmy znacznie większych klawiatur, więc symbole są przetwarzane, gdy ma to sens.
Art

8
Wiele razy napotykałem ten sam problem, ucząc innych języka C i z mojego doświadczenia wynika, że ​​można go rozwiązać w sposób sugerowany przez większość obecnych tu ludzi. Najpierw wyjaśnij pojęcie wskaźnika bez składni C. Następnie naucz składni i kładź nacisk na gwiazdkę jako część typu ( int* p), ostrzegając ucznia przed używaniem wielu deklaracji w tym samym wierszu, gdy w grę wchodzą wskaźniki. Kiedy uczeń w pełni zrozumie pojęcie wskaźników, wyjaśnij mu, że int *pskładnia is jest równoważna, a następnie wyjaśnij problem z wieloma deklaracjami.
Theodoros Chatzigiannakis

Odpowiedzi:


43

Aby Twój uczeń mógł zrozumieć znaczenie *symbolu w różnych kontekstach, musi najpierw zrozumieć, że konteksty są rzeczywiście różne. Kiedy zrozumieją, że konteksty są różne (tj. Różnica między lewą stroną zadania a ogólnym wyrażeniem), zrozumienie różnic nie jest zbyt wielkim skokiem poznawczym.

Najpierw wyjaśnij, że deklaracja zmiennej nie może zawierać operatorów (zademonstruj to, pokazując, że umieszczenie symbolu -lub +w deklaracji zmiennej po prostu powoduje błąd). Następnie pokaż, że wyrażenie (tj. Po prawej stronie przypisania) może zawierać operatory. Upewnij się, że uczestnik kursu rozumie, że wyrażenie i deklaracja zmiennej to dwa zupełnie różne konteksty.

Kiedy zrozumieją, że konteksty są różne, możesz przejść do wyjaśnienia, że ​​kiedy *symbol znajduje się w deklaracji zmiennej przed identyfikatorem zmiennej, oznacza to „zadeklaruj tę zmienną jako wskaźnik”. Następnie możesz wyjaśnić, że *symbol używany w wyrażeniu (jako operator jednoargumentowy) jest „operatorem wyłuskiwania” i oznacza „wartość pod adresem”, a nie jego wcześniejsze znaczenie.

Aby naprawdę przekonać ucznia, wyjaśnij, że twórcy C mogli użyć dowolnego symbolu na oznaczenie operatora wyłuskiwania (tj. Mogli @zamiast tego użyć ), ale z jakiegokolwiek powodu podjęli decyzję o użyciu *.

Podsumowując, nie da się wyjaśnić, że konteksty są różne. Jeśli uczeń nie rozumie różnych kontekstów, nie może zrozumieć, dlaczego *symbol może oznaczać różne rzeczy.


80

Powód, dla którego skrót:

int *bar = &foo;

w twoim przykładzie może być mylące, ponieważ łatwo jest go błędnie odczytać jako równoważny z:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

kiedy to faktycznie oznacza:

int *bar;
bar = &foo;

Napisany w ten sposób, z oddzielną deklaracją zmiennej i przypisaniem, nie ma takiego potencjału do zamieszania, a paralelizm użycia ↔ deklaracji opisany w cytacie K&R działa doskonale:

  • Pierwsza linia deklaruje zmienną bar, taką *barjak int.

  • Druga linia przypisuje adres foodo bar, tworząc *bar(a int) alias dla foo(również int).

Wprowadzając składnię wskaźnika C dla początkujących, pomocne może być początkowo trzymanie się tego stylu oddzielania deklaracji wskaźnika od przypisań i wprowadzenie połączonej składni skróconej (z odpowiednimi ostrzeżeniami o potencjalnym pomyłce) dopiero po wprowadzeniu podstawowych pojęć dotyczących używania wskaźnika w C zostały odpowiednio zinternalizowane.


4
Miałbym ochotę typedef. typedef int *p_int;oznacza, że ​​zmienna typu p_intma właściwość, która *p_intjest int. Wtedy mamy p_int bar = &foo;. Zachęcanie kogokolwiek do tworzenia niezainicjowanych danych i późniejszego przypisywania ich do nich w ramach domyślnego nawyku wydaje się ... złym pomysłem.
Yakk - Adam Nevraumont

6
To tylko uszkodzony mózg deklaracji C; nie jest specyficzne dla wskaźników. Rozważmy int a[2] = {47,11};, że nie jest to inicjalizacja (nieistniejącego) elementu a[2].
Marc van Leeuwen

5
@MarcvanLeeuwen Zgadzam się z uszkodzeniem mózgu. Idealnie, *powinno być częścią typu, a nie przypisanym do zmiennej, a wtedy byłbyś w stanie napisać, int* foo_ptr, bar_ptraby zadeklarować dwa wskaźniki. Ale w rzeczywistości deklaruje wskaźnik i liczbę całkowitą.
Barmar

1
Nie chodzi tylko o „skrócone” deklaracje / przypisania. Cała kwestia pojawia się ponownie w momencie, gdy chcesz użyć wskaźników jako argumentów funkcji.
armin

30

Krótkie deklaracje

Dobrze jest poznać różnicę między deklaracją a inicjalizacją. Deklarujemy zmienne jako typy i inicjalizujemy je wartościami. Jeśli robimy jedno i drugie w tym samym czasie, często nazywamy to definicją.

1. int a; a = 42;

int a;
a = 42;

Możemy zadeklarować o intnazwie A . Następnie inicjalizujemy go, nadając mu wartość 42.

2. int a = 42;

Możemy zadeklarować i intnazwany i nadać mu wartość 42. Jest on inicjowany . Definicja.42

3. a = 43;

Kiedy używamy zmiennych, mówimy, że działamy na nich. a = 43jest operacją przypisania. Liczbę 43 przypisujemy zmiennej a.

Mówiąc

int *bar;

deklarujemy, że bar jest wskaźnikiem do int. Mówiąc

int *bar = &foo;

deklarujemy bar i inicjalizujemy go adresem foo .

Po zainicjowaniu bara możemy użyć tego samego operatora, gwiazdki, aby uzyskać dostęp i operować na wartości foo . Bez operatora uzyskujemy dostęp i działamy na adresie wskazywanym przez wskaźnik.

Poza tym pozwoliłem przemówić obrazowi.

Co

Uproszczone WSPOMNIENIE o tym, co się dzieje. (A tutaj wersja odtwarzacza, jeśli chcesz pauzować itp.)

          WSPOMNIENIE


22

Drugie stwierdzenie int *bar = &foo;można zobaczyć obrazowo w pamięci jako:

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100

Teraz barjest wskaźnikiem typu intzawierającego adres &z foo. Używając operatora jednoargumentowego *, szanujemy, aby pobrać wartość zawartą w „foo” za pomocą wskaźnika bar.

EDYCJA : Moje podejście do początkujących polega na wyjaśnianiu memory addresszmiennej, tj

Memory Address:Każda zmienna ma powiązany z nią adres dostarczony przez system operacyjny. W int a;, &ajest adresem zmiennej a.

Kontynuuj wyjaśnianie podstawowych typów zmiennych w Cas,

Types of variables: Zmienne mogą zawierać wartości odpowiednich typów, ale nie adresy.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 

Introducing pointers: Jak wspomniano powyżej, na przykład zmienne

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR

Możliwe jest przypisanie, b = aale nie b = &a, ponieważ zmienna bmoże mieć wartość, ale nie adres, dlatego wymagamy wskaźników .

Pointer or Pointer variables :Jeśli zmienna zawiera adres, nazywana jest zmienną wskaźnikową. Użyj *w deklaracji, aby poinformować, że jest to wskaźnik.

 Pointer can hold address but not value
 Pointer contains the address of an existing variable.
 Pointer points to an existing variable

3
Problem polega na tym, że czytając int *ipjako „ip jest wskaźnikiem (*) typu int”, masz kłopoty podczas czytania czegoś takiego x = (int) *ip.
armin

2
@abw To coś zupełnie innego, stąd nawiasy. Nie sądzę, żeby ludzie mieli trudności ze zrozumieniem różnicy między deklaracjami a castingami.
bzeaman

@abw W x = (int) *ip;, pobierz wartość przez wyłuskiwanie wskaźnika ipi rzuć wartość na intdowolny typ ip.
Sunil Bojanapally

1
@BennoZeeman Masz rację: casting i deklaracje to dwie różne rzeczy. Próbowałem wskazać inną rolę gwiazdki: 1. "to nie jest int, ale wskaźnik do int" 2. "To da ci int, ale nie wskaźnik do int".
armin

2
@abw: Dlatego nauczanie int* bar = &foo;sprawia ładuje więcej sensu. Tak, wiem, że zadeklarowanie wielu wskaźników w jednej deklaracji powoduje problemy. Nie, nie sądzę, żeby to miało w ogóle znaczenie.
Wyścigi lekkości na orbicie,

17

Patrząc na odpowiedzi i komentarze tutaj, wydaje się, że istnieje ogólna zgoda, że ​​dana składnia może być myląca dla początkującego. Większość z nich proponuje coś podobnego:

  • Przed pokazaniem jakiegokolwiek kodu użyj diagramów, szkiców lub animacji, aby zilustrować działanie wskaźników.
  • Przedstawiając składnię, wyjaśnij dwie różne role symbolu gwiazdki . Brakuje wielu samouczków lub omija tę część. Powstaje zamieszanie ("Kiedy złamiesz zainicjowaną deklarację wskaźnika na deklarację i późniejsze przypisanie, musisz pamiętać o usunięciu *" - comp.lang.c FAQ ) Miałem nadzieję znaleźć alternatywne podejście, ale myślę, że to jest droga do przebycia.

Możesz int* barzamiast tego napisać, int *baraby podkreślić różnicę. Oznacza to, że nie będziesz postępować zgodnie z podejściem K&R „deklaracja naśladuje użycie”, ale podejściem Stroustrup C ++ :

Nie deklarujemy, *barże jesteśmy liczbą całkowitą. Deklarujemy, barże jesteśmy int*. Jeśli chcemy zainicjować nowo utworzoną zmienną w tej samej linii, jasne jest, że mamy do czynienia z bar, a nie *bar.int* bar = &foo;

Wady:

  • Musisz ostrzec swojego ucznia o problemie z deklaracją wielokrotnych wskaźników ( int* foo, barvs int *foo, *bar).
  • Musisz je przygotować na świat bólu . Wielu programistów chce widzieć gwiazdkę obok nazwy zmiennej i bardzo się starają, aby uzasadnić swój styl. I wiele przewodników po stylach wyraźnie wymusza tę notację (styl kodowania jądra Linuksa, przewodnik po stylu C NASA itp.).

Edycja: inne podejście, które zostało zasugerowane, polega na "naśladowaniu" K&R, ale bez "skróconej składni" (zobacz tutaj ). Gdy tylko pominiesz wykonanie deklaracji i przypisania w tej samej linii , wszystko będzie wyglądało o wiele bardziej spójnie.

Jednak wcześniej czy później uczeń będzie musiał zajmować się wskaźnikami jako argumentami funkcji. I wskaźniki jako typy zwracane. I wskaźniki do funkcji. Będziesz musiał wyjaśnić różnicę między int *func();a int (*func)();. Myślę, że prędzej czy później wszystko się rozpadnie. A może wcześniej jest lepiej niż później.


16

Jest powód, dla którego preferuje styl K&R, int *pa styl Stroustrup int* p; oba są ważne (i oznaczają to samo) w każdym języku, ale jak ujął to Stroustrup:

Wybór między "int * p;" i „int * p;” nie dotyczy dobra i zła, ale stylu i nacisku. C podkreślone wyrażenia; deklaracje były często uważane za coś więcej niż zło konieczne. Z drugiej strony C ++ kładzie duży nacisk na typy.

Teraz, skoro próbujesz tutaj uczyć C, sugerowałoby to, że powinieneś bardziej podkreślać wyrażenia niż typy, ale niektórzy ludzie mogą łatwiej przyswoić jeden nacisk szybciej niż drugi, a to raczej o nich niż o języku.

Dlatego niektórym osobom łatwiej będzie zacząć od pomysłu, że a int*to coś innego niż an inti od tego zacząć.

Jeśli ktoś ma szybko grok sposób patrzenia na to, że zastosowanie int* barmają barjako rzecz, która nie jest typu int, ale wskaźnik do int, wtedy będziesz szybko zobaczyć, że *barjest robić coś nabar , a reszta przyjdzie sama. Gdy już to zrobisz, możesz później wyjaśnić, dlaczego programiści C wolą int *bar.

Albo nie. Gdyby istniał jeden sposób, w jaki wszyscy zrozumieliby tę koncepcję, od początku nie mielibyśmy żadnych problemów, a najlepszy sposób na wyjaśnienie go jednej osobie niekoniecznie będzie najlepszym sposobem na wyjaśnienie go drugiej.


1
Podoba mi się argument Stroustrupa, ale zastanawiam się, dlaczego wybrał symbol & do oznaczenia odniesień - kolejna możliwa pułapka.
armin

1
@abw Myślę, że dostrzegł symetrię, jeśli możemy to zrobić, int* p = &amożemy to zrobić int* r = *p. Jestem prawie pewien, że omówił to w The Design and Evolution of C ++ , ale minęło dużo czasu, odkąd to przeczytałem i głupio przekazałem komuś swoją kopię.
Jon Hanna

3
Chyba masz na myśli int& r = *p. I założę się, że pożyczkobiorca wciąż próbuje przetrawić książkę.
armin

@abw, tak, dokładnie to miałem na myśli. Niestety literówki w komentarzach nie powodują błędów kompilacji. Książka jest właściwie dość energicznie czytana.
Jon Hanna

4
Jednym z powodów, dla których wolę składnię Pascala (popularnie rozszerzoną) w stosunku do języka C, jest to, Var A, B: ^Integer;że wyjaśnia, że ​​typ „wskaźnik do liczby całkowitej” odnosi się zarówno do, jak Ai B. Używanie K&Rstylu int *a, *bjest również wykonalne; ale taka deklaracja wygląda int* a,b;jednak tak, jakby była ai bobie są deklarowane jako int*, ale w rzeczywistości deklaruje ajako int*i bjako int.
supercat

9

tl; dr:

P: Jak wyjaśnić początkującym wskaźniki C (deklaracja vs. operatory jednoargumentowe)?

O: nie. Wyjaśnij wskaźniki początkującym i pokaż im, jak przedstawić koncepcje wskaźników w składni języka C po.


Niedawno miałem przyjemność wyjaśnić wskazówki początkującym programistom w C i napotkałem następującą trudność.

IMO składnia C nie jest okropna, ale też nie jest cudowna: nie jest to ani wielka przeszkoda, jeśli już rozumiesz wskaźniki, ani żadna pomoc w ich nauce.

Dlatego: zacznij od wyjaśnienia wskazówek i upewnij się, że naprawdę je rozumieją:

  • Wyjaśnij je na diagramach w kształcie prostokąta i strzałki. Możesz to zrobić bez adresów szesnastkowych, jeśli nie są one istotne, po prostu pokaż strzałki wskazujące inne pole lub jakiś symbol nul.

  • Wyjaśnij pseudokodem: po prostu wpisz adres foo i wartość przechowywaną w bar .

  • Następnie, kiedy nowicjusz zrozumie, czym są wskazówki, dlaczego i jak ich używać; następnie pokaż mapowanie na składnię C.

Podejrzewam, że powodem, dla którego tekst K&R nie dostarcza modelu koncepcyjnego, jest to, że oni już zrozumieli wskaźniki i prawdopodobnie założyli, że co inny kompetentny programista w tamtym czasie też to zrobił. Mnemonik jest tylko przypomnieniem odwzorowania dobrze zrozumiałej koncepcji na składnię.


W rzeczy samej; Zacznij od teorii, składnia pojawi się później (i nie jest ważna). Zauważ, że teoria użycia pamięci nie zależy od języka. Ten model typu „pudełko i strzałki” pomoże Ci wykonywać zadania w dowolnym języku programowania.
2015

Zobacz tutaj kilka przykładów (chociaż Google również pomoże) eskimo.com/~scs/cclass/notes/sx10a.html
oɔɯǝɹ

7

Ten problem jest nieco zagmatwany, gdy zaczynasz uczyć się C.

Oto podstawowe zasady, które mogą Ci pomóc w rozpoczęciu:

  1. W C jest tylko kilka podstawowych typów:

    • char: wartość całkowita o rozmiarze 1 bajtu.

    • short: wartość całkowita o rozmiarze 2 bajtów.

    • long: wartość całkowita o rozmiarze 4 bajtów.

    • long long: wartość całkowita o rozmiarze 8 bajtów.

    • float: wartość niecałkowita o rozmiarze 4 bajtów.

    • double: wartość niecałkowita o rozmiarze 8 bajtów.

    Zauważ, że rozmiar każdego typu jest ogólnie definiowany przez kompilator, a nie przez standard.

    Te liczby całkowite short, longi long longsą zwykle następuje int.

    Nie jest to jednak konieczne i można ich używać bez int.

    Alternatywnie możesz po prostu stwierdzić int, ale może to być inaczej interpretowane przez różne kompilatory.

    Podsumowując:

    • shortjest taki sam jak, short intale niekoniecznie taki sam jak int.

    • longjest taki sam jak, long intale niekoniecznie taki sam jak int.

    • long longjest taki sam jak, long long intale niekoniecznie taki sam jak int.

    • W danym kompilatorze intjest albo short intalbo long intalbo long long int.

  2. Jeśli deklarujesz zmienną pewnego typu, możesz również zadeklarować inną zmienną wskazującą na nią.

    Na przykład:

    int a;

    int* b = &a;

    Zasadniczo dla każdego typu podstawowego mamy również odpowiedni typ wskaźnika.

    Na przykład: shorti short*.

    Istnieją dwa sposoby "spojrzenia" na zmienną b (to prawdopodobnie dezorientuje większość początkujących) :

    • Możesz rozważyć bjako zmienną typu int*.

    • Możesz rozważyć *bjako zmienną typu int.

    Dlatego niektórzy ludzie deklarowaliby int* b, podczas gdy inni deklarowali int *b.

    Ale faktem jest, że te dwie deklaracje są identyczne (spacje są bez znaczenia).

    Można użyć bjako wskaźnika do wartości całkowitej lub *bjako rzeczywistej wskazanej liczby całkowitej.

    Można dostać (odczyt) wartości spiczasty: int c = *b.

    I można ustawić (zapis) wartość spiczasty: *b = 5.

  3. Wskaźnik może wskazywać na dowolny adres pamięci, a nie tylko na adres jakiejś zmiennej, którą wcześniej zadeklarowałeś. Należy jednak zachować ostrożność podczas używania wskaźników, aby uzyskać lub ustawić wartość znajdującą się pod wskazanym adresem pamięci.

    Na przykład:

    int* a = (int*)0x8000000;

    Tutaj mamy zmienną awskazującą na adres pamięci 0x8000000.

    Jeśli ten adres pamięci nie jest zamapowany w przestrzeni pamięci twojego programu, to każda operacja odczytu lub zapisu *anajprawdopodobniej spowoduje awarię programu z powodu naruszenia zasad dostępu do pamięci.

    Możesz bezpiecznie zmienić wartość a, ale powinieneś być bardzo ostrożny, zmieniając wartość *a.

  4. Typ void*jest wyjątkowy, ponieważ nie ma odpowiedniego „typu wartości”, którego można użyć (tj. Nie można zadeklarować void a). Ten typ jest używany tylko jako ogólny wskaźnik do adresu pamięci, bez określania typu danych, które znajdują się w tym adresie.


7

Być może przejście przez to trochę bardziej ułatwi to:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}

Poproś, aby powiedzieli ci, czego oczekują, że dane wyjściowe będą w każdej linii, a następnie niech uruchomią program i zobaczą, co się pojawi. Wyjaśnij ich pytania (naga wersja z pewnością podpowie kilka - ale możesz później martwić się o styl, surowość i przenośność). Następnie, zanim ich umysł zamieni się w papkę z przemyślenia lub staną się zombie po obiedzie, napisz funkcję, która przyjmuje wartość, i taką samą, która przyjmuje wskaźnik.

Z mojego doświadczenia wynika, że ​​„dlaczego to jest drukowane w ten sposób?” garb, a następnie od razu pokazując, dlaczego jest to przydatne w parametrach funkcji, poprzez praktyczne zabawy (jako wstęp do niektórych podstawowych materiałów K&R, takich jak analiza ciągów / przetwarzanie tablic), co sprawia, że ​​lekcja nie tylko ma sens, ale się trzyma.

Następnym krokiem jest przekonanie ich, aby wyjaśnili ci, jak się z tym i[0]wiąże &i. Jeśli będą w stanie to zrobić, nie zapomną o tym i możesz zacząć mówić o strukturach, nawet trochę wcześniej, tak aby to się zapadło.

Powyższe zalecenia dotyczące skrzynek i strzał są również dobre, ale mogą również zakończyć się dygresją do pełnej dyskusji o tym, jak działa pamięć - która jest rozmową, która musi się wydarzyć w pewnym momencie, ale może odwrócić uwagę od tego, co jest w zasięgu ręki. : jak interpretować notację wskaźnikową w C.


To dobre ćwiczenie. Ale kwestia, którą chciałem poruszyć, to specyficzna syntaktyczna kwestia, która może mieć wpływ na model mentalny budowany przez uczniów. Rozważ to: int foo = 1;. Teraz jest OK: int *bar; *bar = foo;. To nie jest w porządku:int *bar = foo;
armin

1
@abw Jedyną rzeczą, która ma sens, jest to, co uczniowie w końcu mówią sobie. To znaczy „zobacz jednego, zrób jedno, naucz jednego”. Nie możesz chronić ani przewidywać, jaką składnię lub styl zobaczą w dżungli (nawet w starych repozytoriach!), Więc musisz pokazać wystarczającą liczbę permutacji, aby podstawowe pojęcia były zrozumiałe niezależnie od stylu - i następnie zacznij ich uczyć, dlaczego ustalono pewne style. Podobnie jak w przypadku nauczania języka angielskiego: podstawowe wyrażenia, idiomy, style, poszczególne style w określonym kontekście. Niestety nie jest to łatwe. W każdym razie powodzenia!
zxq9

6

Typ wyrażenia *bar to int; w ten sposób typ zmiennej (i wyrażenia) barto int *. Ponieważ zmienna ma typ wskaźnikowy, jej inicjator również musi mieć typ wskaźnika.

Istnieje niespójność między inicjalizacją a przypisaniem zmiennej wskaźnikowej; to jest coś, czego trzeba się nauczyć na własnej skórze.


3
Patrząc na odpowiedzi tutaj, mam wrażenie, że wielu doświadczonych programistów nie widzi już problemu . Wydaje mi się, że jest to produkt uboczny „nauki życia z niekonsekwencjami”.
armin

3
@abw: zasady inicjalizacji różnią się od reguł przypisywania; dla skalarnych typów arytmetycznych różnice są pomijalne, ale mają one znaczenie dla typów wskaźnikowych i zagregowanych. To jest coś, co musisz wyjaśnić wraz ze wszystkim innym.
John Bode,

5

Wolałbym przeczytać to, ponieważ pierwszy *dotyczy intwięcej niż bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value

2
Następnie musisz wyjaśnić, dlaczego int* a, bnie robi tego, co według nich robi.
Pharap

4
To prawda, ale nie sądzę, int* a,baby w ogóle tego używać. Dla lepszej czytelności, aktualizacji itp ... w każdym wierszu powinna znajdować się tylko jedna deklaracja zmiennej i nigdy więcej. Jest to również coś do wyjaśnienia początkującym, nawet jeśli kompilator sobie z tym poradzi.
grorel

To jednak opinia jednego mężczyzny. Istnieją miliony programistów, którzy nie mają nic przeciwko deklarowaniu więcej niż jednej zmiennej w wierszu i robią to codziennie w ramach swojej pracy. Nie możesz ukryć uczniów przed alternatywnymi sposobami robienia rzeczy, lepiej pokazać im wszystkie alternatywy i pozwolić im zdecydować, w którą stronę chcą coś zrobić, ponieważ jeśli kiedykolwiek zostaną zatrudnieni, będą musieli podążać za pewnym stylem, który mogą, ale nie muszą być z nimi wygodne. Dla programisty wszechstronność to bardzo dobra cecha.
Pharap

1
Zgadzam się z @grorel. Łatwiej jest myśleć o tym *jako o typie i po prostu zniechęcać int* a, b. Chyba że wolisz mówić, że *ato intraczej typ niż awskazówka do int...
Kevin Ushey,

@grorel ma rację: int *a, b;nie powinno być używane. Deklarowanie dwóch zmiennych o różnych typach w tym samym oświadczeniu jest dość kiepską praktyką i silnym kandydatem do problemów związanych z konserwacją. Być może jest inaczej dla tych z nas, którzy pracują w osadzonym polu, gdzie a int*i a intmają często różne rozmiary i czasami są przechowywane w zupełnie innych lokalizacjach pamięci. Jest to jeden z wielu aspektów języka C, którego najlepiej byłoby uczyć jako „wolno, ale nie rób tego”.
Evil Dog Pie

5
int *bar = &foo;

Question 1: Co to jest bar?

Ans: Jest to zmienna wskaźnikowa (do wpisania int). Wskaźnik powinien wskazywać na jakąś prawidłową lokalizację w pamięci, a później powinien zostać wyłuskany (* bar) przy użyciu operatora jednoargumentowego *w celu odczytania wartości przechowywanej w tej lokalizacji.

Question 2: Co to jest &foo?

Ans: foo jest zmienną typu., która intjest przechowywana w jakiejś poprawnej lokalizacji w pamięci i tej lokalizacji otrzymujemy od operatora, &więc teraz mamy jakąś prawidłową lokalizację w pamięci &foo.

Tak więc oba razem wzięte, tj. To, czego potrzebował wskaźnik, było prawidłową lokalizacją w pamięci i zostało &footo osiągnięte, więc inicjalizacja jest dobra.

Teraz wskaźnik barwskazuje na prawidłowe miejsce w pamięci, a wartość w nim przechowywana może zostać odczytana, tj*bar


5

Powinieneś zwrócić uwagę początkującego, że * ma inne znaczenie w deklaracji i wyrażeniu. Jak wiesz, * w wyrażeniu jest operatorem jednoargumentowym, a * w deklaracji nie jest operatorem, a jedynie rodzajem składni łączącej się z typem, aby kompilator wiedział, że jest to typ wskaźnikowy. lepiej powiedzieć początkującym, „* ma inne znaczenie. Aby zrozumieć znaczenie *, powinieneś znaleźć miejsce, w którym występuje *”


4

Myślę, że diabeł jest w kosmosie.

Napisałbym (nie tylko dla początkujących, ale także dla siebie): int * bar = & foo; zamiast int * bar = & foo;

Powinno to wyjaśnić, jaki jest związek między składnią a semantyką


4

Jak już wspomniano, * ma wiele ról.

Jest jeszcze jeden prosty pomysł, który może pomóc początkującym w zrozumieniu rzeczy:

Pomyśl, że „=” ma również wiele ról.

Gdy przypisanie jest używane w tym samym wierszu z deklaracją, należy traktować je jako wywołanie konstruktora, a nie dowolne przypisanie.

Kiedy widzisz:

int *bar = &foo;

Pomyśl, że jest to prawie równoważne z:

int *bar(&foo);

Nawiasy mają pierwszeństwo przed gwiazdką, więc „& foo” znacznie łatwiej intuicyjnie przypisać do „bar” niż „* bar”.


4

Widziałem to pytanie kilka dni temu, a potem akurat czytałem wyjaśnienie deklaracji typu Go na blogu Go . Zaczyna się od podania deklaracji typu C, co wydaje się przydatnym źródłem do dodania do tego wątku, chociaż myślę, że są już podane bardziej kompletne odpowiedzi.

C przyjął niezwykłe i sprytne podejście do składni deklaracji. Zamiast opisywać typy za pomocą specjalnej składni, pisze się wyrażenie obejmujące deklarowaną pozycję i określa, jaki typ będzie miało to wyrażenie. A zatem

int x;

deklaruje x jako int: wyrażenie „x” będzie miało typ int. Ogólnie rzecz biorąc, aby dowiedzieć się, jak zapisać typ nowej zmiennej, napisz wyrażenie obejmujące tę zmienną, której wynikiem jest typ podstawowy, a następnie umieść typ podstawowy po lewej stronie, a wyrażenie po prawej.

Stąd deklaracje

int *p;
int a[3];

stwierdzaj, że p jest wskaźnikiem do int, ponieważ '* p' ma typ int, a a jest tablicą int, ponieważ a [3] (ignorując określoną wartość indeksu, która jest oznaczana jako rozmiar tablicy) ma typ int.

(Dalej opisano, jak rozszerzyć to zrozumienie na wskaźniki funkcji itp.)

Jest to sposób, o którym wcześniej nie myślałem, ale wydaje się, że jest to całkiem prosty sposób uwzględnienia przeciążenia składni.


3

Jeśli problemem jest składnia, pomocne może być pokazanie równoważnego kodu z szablonem / using.

template<typename T>
using ptr = T*;

To może być następnie używane jako

ptr<int> bar = &foo;

Następnie porównaj normalną składnię / C z tym podejściem tylko w C ++. Jest to również przydatne przy wyjaśnianiu wskaźników stałych.


2
Dla początkujących będzie to o wiele bardziej zagmatwane.
Karsten,

Myślałem, że nie pokazałeś definicji ptr. Po prostu użyj go do deklaracji wskaźnika.
MI3Guy

3

Źródło nieporozumień wynika z faktu, że *symbol może mieć różne znaczenia w języku C, w zależności od faktu, w jakim jest używany. Aby wytłumaczyć wskaźnik początkującemu, *należy wyjaśnić znaczenie symbolu w innym kontekście.

W deklaracji

int *bar = &foo;  

*symbol nie operatorowi wskazanie pośrednie . Zamiast tego pomaga określić typbarinformowania kompilatora, którybarjest wskaźnikiem do plikuint . Z drugiej strony, kiedy pojawia się w instrukcji,*symbol (używany jako operator jednoargumentowy ) działa pośrednio. Dlatego oświadczenie

*bar = &foo;

byłby błędny, ponieważ przypisuje adres fooobiektowi, który barwskazuje, a nie barsamemu sobie.


3

„może zapisanie tego jako int * bar sprawia, że ​​jest bardziej oczywiste, że gwiazda jest w rzeczywistości częścią typu, a nie częścią identyfikatora”. Ja również. I mówię, że to trochę jak Type, ale tylko dla jednej nazwy wskaźnika.

„Oczywiście powoduje to różne problemy związane z nieintuicyjnymi rzeczami, takimi jak int * a, b”.


2

Tutaj musisz użyć, zrozumieć i wyjaśnić logikę kompilatora, a nie logikę człowieka (wiem, jesteś człowiekiem, ale tutaj musisz naśladować komputer ...).

Kiedy piszesz

int *bar = &foo;

grupy kompilatorów, które jako

{ int * } bar = &foo;

To znaczy: oto nowa zmienna, jej nazwa to bar, jej typ to wskaźnik do int, a jej wartość początkowa to &foo.

I trzeba dodać: =powyższe oznacza inicjalizację, a nie afektację, podczas gdy w kolejnych wyrażeniach *bar = 2;tak jest affectation

Edytuj według komentarza:

Uwaga: w przypadku wielokrotnych deklaracji *dotyczy tylko następującej zmiennej:

int *bar = &foo, b = 2;

bar jest wskaźnikiem do int zainicjalizowanym przez adres foo, b jest int zainicjowanym na 2, a in

int *bar=&foo, **p = &bar;

bar w nieruchomym wskaźniku do int, a p jest wskaźnikiem do wskaźnika do int zainicjalizowanego na adres lub pasek.


2
Właściwie kompilator nie grupuje tego w ten sposób: int* a, b;deklaruje a jako wskaźnik do an int, ale b jako int. *Symbol ma tylko dwa różne znaczenia: W deklaracji, oznacza to typ wskaźnika, a wyrazem jest to jednoskładnikowa operator dereference.
tmlen

@tmlen: Chodziło mi o to, że podczas inicjalizacji *in grzechotał się do typu, tak że wskaźnik jest inicjalizowany, podczas gdy w działaniu afektywnym ma to wpływ na wskazaną wartość. Ale przynajmniej dałeś mi fajną czapkę :-)
Serge Ballesta

0

Zasadniczo wskaźnik nie jest wskazaniem tablicy. Początkujący łatwo myśli, że wskaźnik wygląda jak tablica. większość przykładów ciągów przy użyciu rozszerzenia

"char * pstr" wygląda podobnie

„char str [80]”

Ale ważne rzeczy, Pointer jest traktowany jako zwykła liczba całkowita na niższym poziomie kompilatora.

Spójrzmy na przykłady:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **env)
{
    char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address

    char *pstr0 = str;   // or this will be using with
    // or
    char *pstr1 = &str[0];

    unsigned int straddr = (unsigned int)pstr0;

    printf("Pointer examples: pstr0 = %08x\n", pstr0);
    printf("Pointer examples: &str[0] = %08x\n", &str[0]);
    printf("Pointer examples: str = %08x\n", str);
    printf("Pointer examples: straddr = %08x\n", straddr);
    printf("Pointer examples: str[0] = %c\n", str[0]);

    return 0;
}

Wyniki będą podobne do tego 0x2a6b7ed0 to adres str []

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

Zasadniczo pamiętaj, że wskaźnik jest rodzajem liczby całkowitej. przedstawienie adresu.


-1

Wyjaśniłbym, że ints są obiektami, podobnie jak pływaki itp. Wskaźnik jest typem obiektu, którego wartość reprezentuje adres w pamięci (stąd dlaczego wskaźnik domyślnie ma wartość NULL).

Kiedy po raz pierwszy deklarujesz wskaźnik, używasz składni typu-wskaźnika-nazwy. Jest odczytywany jako „wskaźnik całkowity o nazwie nazwa, który może wskazywać na adres dowolnego obiektu będącego liczbą całkowitą”. Używamy tej składni tylko podczas dekleracji, podobnie jak w przypadku deklarowania int jako „int num1”, ale „num1” używamy tylko wtedy, gdy chcemy użyć tej zmiennej, a nie „int num1”.

int x = 5; // obiekt będący liczbą całkowitą o wartości 5

int * ptr; // liczba całkowita z wartością domyślną NULL

Aby wskaźnik wskazywał na adres obiektu, używamy symbolu „&”, który można odczytać jako „adres”.

ptr = & x; // teraz wartością jest adres 'x'

Ponieważ wskaźnik jest tylko adresem obiektu, aby uzyskać rzeczywistą wartość przechowywaną pod tym adresem, musimy użyć symbolu „*”, który użyty przed wskaźnikiem oznacza „wartość pod adresem wskazywanym przez”.

std :: cout << * ptr; // wypisuje wartość pod adresem

Możesz krótko wyjaśnić, że ' ” to „operator”, który zwraca różne wyniki z różnymi typami obiektów. Użyty ze wskaźnikiem operator ” nie oznacza już „mnożonego przez”.

Pomaga narysować diagram pokazujący, jak zmienna ma nazwę i wartość, a wskaźnik ma adres (nazwę) i wartość, i pokazać, że wartość wskaźnika będzie adresem int.


-1

Wskaźnik to po prostu zmienna używana do przechowywania adresów.

Pamięć w komputerze składa się z bajtów (bajt składa się z 8 bitów) ułożonych w sposób sekwencyjny. Każdy bajt ma przypisaną liczbę, podobnie jak indeks lub indeks w tablicy, która jest nazywana adresem bajtu. Adres bajtu zaczyna się od 0 do jednego mniej niż rozmiar pamięci. Na przykład, powiedzmy, że w 64 MB pamięci RAM jest 64 * 2 ^ 20 = 67108864 bajtów. Dlatego adres tych bajtów będzie zaczynał się od 0 do 67108863.

wprowadź opis obrazu tutaj

Zobaczmy, co się stanie, gdy zadeklarujesz zmienną.

znaki int;

Jak wiemy, int zajmuje 4 bajty danych (zakładając, że używamy kompilatora 32-bitowego), więc kompilator rezerwuje 4 kolejne bajty z pamięci do przechowywania wartości całkowitej. Adres pierwszego bajtu z 4 przydzielonych bajtów jest znany jako adres znaków zmiennych. Powiedzmy, że adres 4 kolejnych bajtów to 5004, 5005, 5006 i 5007, to adres zmiennych znaczników będzie wynosił 5004. wprowadź opis obrazu tutaj

Deklarowanie zmiennych wskaźnikowych

Jak już powiedziano, wskaźnik jest zmienną, która przechowuje adres pamięci. Podobnie jak w przypadku innych zmiennych, musisz najpierw zadeklarować zmienną wskaźnikową, zanim będziesz mógł jej użyć. Oto jak możesz zadeklarować zmienną wskaźnika.

Składnia: data_type *pointer_name;

typ_danych to typ wskaźnika (znany również jako typ podstawowy wskaźnika). nazwa_wskaźnika to nazwa zmiennej, która może być dowolnym poprawnym identyfikatorem C.

Weźmy kilka przykładów:

int *ip;

float *fp;

int * ip oznacza, że ​​ip jest zmienną wskaźnikową, która może wskazywać na zmienne typu int. Innymi słowy, zmienna wskaźnikowa ip może przechowywać tylko adresy zmiennych typu int. Podobnie, zmienna wskaźnikowa fp może przechowywać tylko adres zmiennej typu float. Typ zmiennej (znany również jako typ bazowy) ip jest wskaźnikiem do int, a typ fp jest wskaźnikiem do float. Zmienna wskaźnikowa typu wskaźnik do int może być reprezentowana symbolicznie jako (int *). Podobnie, zmienna wskaźnikowa typu pointer to float może być reprezentowana jako (float *)

Po zadeklarowaniu zmiennej wskaźnikowej następnym krokiem jest przypisanie jej prawidłowego adresu pamięci. Nigdy nie powinieneś używać zmiennej wskaźnikowej bez przypisania jej jakiegoś prawidłowego adresu pamięci, ponieważ tuż po deklaracji zawiera ona wartość śmieciową i może wskazywać na dowolne miejsce w pamięci. Użycie nieprzypisanego wskaźnika może dać nieprzewidywalny wynik. Może nawet spowodować awarię programu.

int *ip, i = 10;
float *fp, f = 12.2;

ip = &i;
fp = &f;

Źródło: thecguru jest zdecydowanie najprostszym, ale szczegółowym wyjaśnieniem, jakie kiedykolwiek znalazłem.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.