Wskaźnik do wyjaśnienia wskaźnika

142

Śledziłem ten samouczek o tym, jak działa wskaźnik do wskaźnika .

Zacytuję odpowiedni fragment:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

Teraz możemy ustawić

    int **ipp = &ip1;

i ippwskazuje, na ip1które wskazuje i. *ippjest ip1, i **ippjest i, lub 5. Możemy zilustrować sytuację za pomocą naszej znanej notacji prostokątnej, na przykład:

wprowadź opis obrazu tutaj

Jeśli to powiemy

    *ipp = ip2;

zmieniliśmy wskaźnik wskazywany przez ipp(to znaczy ip1), aby zawierał kopię ip2, tak że ( ip1) wskazuje teraz na j:

wprowadź opis obrazu tutaj


Moje pytanie brzmi: dlaczego na drugim zdjęciu ippnadal wskazuje, ip1ale nie ip2?

Blake
źródło
6
proszę dodać typ ippprzy definiowaniu, aby Twoje pytanie było kompletne ;-)
zmo
1
Ponieważ ippwskazanie na ip1to *ipp = ip2jest takie samo, jakip1 = ip2
Grijesh Chauhan,
1
Czy możemy przestać pisać asteriki tak daleko od spiczastego typu? int **ippjest znacznie mniej intuicyjny niż int** ipp, zwłaszcza gdy **ippoznacza zupełnie inną rzecz poza deklaracjami typów.
Darkhogg
4
@Darkhogg int **ippwydaje mi się dość intuicyjny. To znaczy, robię **ippplik int. To prawda.
ithisa
2
Poza znaczeniami @ user54609 i osobistą intuicyjnością, weź pod uwagę to: typ ippjest int**, więc po prostu pisz int**zamiast magicznego impdereferencja jest int” zrozumieniem .
Manu343726,

Odpowiedzi:

143

Zapomnij na chwilę o analogii wskazującej. To, co naprawdę zawiera wskaźnik, to adres pamięci. Jest &to operator „adres” - tj. Zwraca adres w pamięci obiektu. *Operator daje przedmiot wskaźnik dotyczy, czyli biorąc pod uwagę wskaźnik zawierający adres, to zwraca obiekt pod tym adresem pamięci. Więc kiedy to robisz *ipp = ip2, to, co robisz, polega na *ipppobraniu obiektu pod adresem przechowywanym pod ippktórym jest, ip1a następnie przypisaniu do ip1wartości przechowywanej w ip2, czyli adresie j.

Po prostu
& -> Adres
*-> Wartość at

Robert S. Barnes
źródło
14
& and * nigdy nie były takie proste
Ray,
7
Uważam, że głównym źródłem nieporozumień jest niejednoznaczność operatora *, który podczas deklaracji zmiennej służy do wskazania, że ​​zmienna w rzeczywistości jest wskaźnikiem do określonego typu danych. Ale z drugiej strony jest również używany w instrukcjach do uzyskiwania dostępu do zawartości zmiennej wskazywanej przez wskaźnik (operator dereferencji).
Lucas A.
43

Ponieważ zmieniłeś wartość wskazywaną przez ippnie wartość ipp. Tak więc, ippnadal wskazuje na ip1(wartość ipp), ip1wartość jest teraz taka sama jak ip2wartość, więc oba wskazują na j.

To:

*ipp = ip2;

jest taki sam jak:

ip1 = ip2;
Skizz
źródło
11
Może warto zwrócić uwagę na różnicę między int *ip1 = &ii *ipp = ip2;, tj. Jeśli usuniesz intz pierwszej instrukcji, to przypisania będą wyglądać bardzo podobnie, ale *w obu przypadkach robi się coś zupełnie innego.
Crowman
22

Podobnie jak w przypadku większości pytań dla początkujących w tagu C, na to pytanie można odpowiedzieć, wracając do pierwszych zasad:

  • Wskaźnik to rodzaj wartości.
  • Zmienna zawiera wartość.
  • &Operator włącza zmienną do wskaźnika.
  • *Operator włącza wskaźnik do zmiennej.

(Z technicznego punktu widzenia powinienem powiedzieć „lwartość” zamiast „zmienna”, ale wydaje mi się, że bardziej jasne jest opisanie zmiennych lokalizacji pamięci jako „zmiennych”).

Mamy więc zmienne:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

Zmienna ip1 zawiera wskaźnik. &Operator włącza isię wskaźnik i że wartość wskaźnika jest przypisany ip1. Więc ip1 zawiera wskaźnik do i.

Zmienna ip2 zawiera wskaźnik. &Operator włącza jsię wskaźnik i że wskaźnik jest przypisany ip2. Więc ip2 zawiera wskaźnik do j.

int **ipp = &ip1;

Zmienna ippzawiera wskaźnik. &Operator włącza zmienną ip1do wskaźnika i że wartość wskaźnika jest przypisany ipp. Więc ippzawiera wskaźnik do ip1.

Podsumujmy dotychczasową historię:

  • i zawiera 5
  • j zawiera 6
  • ip1zawiera „wskaźnik do i
  • ip2zawiera „wskaźnik do j
  • ippzawiera „wskaźnik do ip1

Teraz mówimy

*ipp = ip2;

*Operator włącza wskaźnik wstecz do zmiennej. Pobieramy wartość ipp, czyli „wskaźnik do ip1i zamieniamy ją w zmienną. Jaka zmienna? ip1Oczywiście!

Dlatego jest to po prostu inny sposób powiedzenia

ip1 = ip2;

Więc pobieramy wartość ip2. Co to jest? „wskaźnik do j”. Przypisujemy wartość wskaźnika do ip1, więc ip1teraz jest to „wskaźnik do j

Zmieniliśmy tylko jedno: wartość ip1:

  • i zawiera 5
  • j zawiera 6
  • ip1zawiera „wskaźnik do j
  • ip2zawiera „wskaźnik do j
  • ippzawiera „wskaźnik do ip1

Dlaczego ippnadal wskazuje, ip1a nie ip2?

Zmienna zmienia się po przypisaniu do niej. Policz zadania; nie może być więcej zmian w zmiennych niż przypisań! Zaczynasz poprzez przypisanie i, j, ip1, ip2i ipp. Następnie przypisujesz do *ipp, co, jak widzieliśmy, oznacza to samo, co „przypisuj do ip1”. Ponieważ nie przypisałeś go ipppo raz drugi, to się nie zmieniło!

Jeśli chcesz zmienić ipp, musisz faktycznie przypisać do ipp:

ipp = &ip2;

na przykład.

Eric Lippert
źródło
21

mam nadzieję, że ten fragment kodu może pomóc.

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

wyprowadza:

wprowadź opis obrazu tutaj

michaeltang
źródło
12

Osobiście uważam, że zdjęcia ze strzałkami skierowanymi w tę stronę lub takie, które utrudniają zrozumienie wskazówek. Sprawia, że ​​wydają się abstrakcyjnymi, tajemniczymi istotami. Oni nie są.

Jak wszystko inne w twoim komputerze, wskaźniki są liczbami . Nazwa „wskaźnik” to po prostu fantazyjny sposób powiedzenia „zmiennej zawierającej adres”.

Dlatego pozwolę sobie poruszyć różne kwestie, wyjaśniając, jak właściwie działa komputer.

Mamy int, ma nazwę ii wartość 5. To jest przechowywane w pamięci. Podobnie jak wszystko zapisane w pamięci, potrzebuje adresu, inaczej nie bylibyśmy w stanie go znaleźć. Powiedzmy, że ikończy się pod adresem 0x12345678, a jego kolega jz wartością 6 kończy się tuż po nim. Zakładając 32-bitowy procesor, w którym int wynosi 4 bajty, a wskaźniki 4 bajty, wówczas zmienne są przechowywane w pamięci fizycznej w następujący sposób:

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

Teraz chcemy wskazać na te zmienne. Tworzymy jeden wskaźnik do int int* ip1, i jeden int* ip2. Podobnie jak wszystko w komputerze, te zmienne wskaźnikowe są również przydzielane gdzieś w pamięci. Załóżmy, że kończą się one pod następnymi sąsiednimi adresami w pamięci, zaraz po j. ip1=&i;Ustawiliśmy wskaźniki tak, aby zawierały adresy wcześniej przydzielonych zmiennych: („skopiuj adres i do ip1”) i ip2=&j. To, co dzieje się między wierszami, to:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

Więc to, co otrzymaliśmy, to jeszcze jakieś 4-bajtowe fragmenty pamięci zawierające liczby. Nigdzie w zasięgu wzroku nie ma mistycznych ani magicznych strzał.

W rzeczywistości, patrząc na zrzut pamięci, nie możemy stwierdzić, czy adres 0x12345680 zawiera znak intlub int*. Różnica polega na tym, w jaki sposób nasz program korzysta z treści przechowywanych pod tym adresem. (Zadaniem naszego programu jest właściwie po prostu powiedzieć procesorowi, co ma zrobić z tymi liczbami.)

Następnie dodajemy kolejny poziom pośrednictwa za pomocą int** ipp = &ip1;. Znowu otrzymujemy po prostu kawałek pamięci:

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

Wzór wydaje się znajomy. Kolejny fragment 4 bajtów zawierający liczbę.

Teraz, gdybyśmy mieli zrzut pamięci powyższej fikcyjnej małej pamięci RAM, moglibyśmy ręcznie sprawdzić, gdzie wskazują te wskaźniki. Sprawdzamy, co jest przechowywane pod adresem ippzmiennej i znajdujemy zawartość 0x12345680. Który jest oczywiście adresem, pod którym ip1jest przechowywany. Możemy udać się pod ten adres, sprawdzić tam zawartość i znaleźć adres i, a na koniec udać się pod ten adres i znaleźć numer 5.

Więc jeśli weźmiemy zawartość ipp, *ippotrzymamy adres zmiennej wskaźnikowej ip1. Pisząc *ipp=ip2, kopiujemy ip2 do ip1, jest to równoważne ip1=ip2. W obu przypadkach otrzymalibyśmy

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(Te przykłady zostały podane dla procesora typu big endian)

Lundin
źródło
5
Chociaż rozumiem twój punkt widzenia, myślenie o wskaźnikach jako abstrakcyjnych, tajemniczych bytach ma wartość. Każda konkretna implementacja wskaźników to tylko liczby, ale szkicowana strategia implementacji nie jest wymogiem implementacji, to tylko powszechna strategia. Wskaźniki nie muszą być tego samego rozmiaru co liczba int, wskaźniki nie muszą być adresami w płaskim modelu pamięci wirtualnej i tak dalej; to są tylko szczegóły implementacji.
Eric Lippert
@EricLippert Myślę, że można uczynić ten przykład bardziej abstrakcyjnym, nie używając rzeczywistych adresów pamięci lub bloków danych. Gdyby była to tabela stwierdzająca coś w rodzaju location, value, variablelokalizacji 1,2,3,4,5i wartości A,1,B,C,3, odpowiednią ideę wskaźników można łatwo wyjaśnić bez użycia strzałek, które są z natury mylące. Bez względu na wybraną implementację, w jakimś miejscu istnieje wartość i jest to element układanki, który zostaje zaciemniony podczas modelowania za pomocą strzałek.
MirroredFate
@EricLippert Z mojego doświadczenia wynika, że ​​większość przyszłych programistów C, którzy mają problemy ze zrozumieniem wskaźników, to ci, którym karmiono abstrakcyjne, sztuczne modele. Abstrakcja nie jest pomocna, ponieważ głównym celem dzisiejszego języka C jest bliskość sprzętu. Jeśli uczysz się C, ale nie zamierzasz pisać kodu blisko sprzętu, tracisz czas . Java itp. Jest znacznie lepszym wyborem, jeśli nie chcesz wiedzieć, jak działają komputery, ale po prostu programujesz na wysokim poziomie.
Lundin,
@EricLippert I tak, mogą istnieć różne niejasne implementacje wskaźników, w których wskaźniki niekoniecznie odpowiadają adresom. Ale rysowanie strzałek też nie pomoże ci zrozumieć, jak one działają. W pewnym momencie musisz porzucić abstrakcyjne myślenie i przejść do poziomu sprzętu, w przeciwnym razie nie powinieneś używać C. Istnieje wiele znacznie bardziej odpowiednich, nowoczesnych języków przeznaczonych do czysto abstrakcyjnego programowania na wysokim poziomie.
Lundin,
@Lundin: Nie jestem też wielkim fanem diagramów strzałkowych; Pojęcie strzały jako danych jest trudne. Wolę myśleć o tym abstrakcyjnie, ale bez strzał. &Operator o zmiennej daje monetę, która reprezentuje tę zmienną. *Operator na tej monety daje kopię zmiennej. Nie potrzeba strzał!
Eric Lippert,
8

Zwróć uwagę na zadania:

ipp = &ip1;

wyniki ippdo wskazania ip1.

więc do ippdo do punktu ip2, powinniśmy zmienić w podobny sposób,

ipp = &ip2;

czego najwyraźniej nie robimy. Zamiast tego zmieniamy wartość pod adresem wskazanym przez ipp.
Wykonując następujące czynności

*ipp = ip2;

po prostu zastępujemy przechowywaną wartość ip1.

ipp = &ip1, Środki *ipp = ip1 = &i,
Teraz *ipp = ip2 = &j.
Więc *ipp = ip2jest zasadniczo taki sam jak ip1 = ip2.

Dipto
źródło
5
ipp = &ip1;

Żadne później przypisanie nie zmieniło wartości ipp. Dlatego nadal wskazuje ip1.

To, co robisz *ipp, czyli z ip1, nie zmienia faktu, na który ippwskazuje ip1.

Daniel Daranas
źródło
5

Moje pytanie brzmi: dlaczego na drugim obrazku ipp nadal wskazuje na ip1, ale nie na ip2?

umieściłeś ładne zdjęcia, spróbuję zrobić fajną sztukę ascii:

Jak @ Robert-S-Barnes powiedział w swojej odpowiedzi: zapomnij o wskazówkach io tym, co wskazuje na co, ale myśl w kategoriach pamięci. Zasadniczo int*oznacza to, że zawiera adres zmiennej, a an int**zawiera adres zmiennej, która zawiera adres zmiennej. Następnie możesz użyć algebry wskaźnika, aby uzyskać dostęp do wartości lub adresów: &foośrednich address of fooi *foośrednich value of the address contained in foo.

Tak więc, ponieważ wskaźniki dotyczą radzenia sobie z pamięcią, najlepszym sposobem na uczynienie tego „namacalnym” jest pokazanie, co algebra wskaźników robi z pamięcią.

Oto pamięć twojego programu (uproszczona na potrzeby przykładu):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

kiedy robisz swój początkowy kod:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

tak wygląda twoja pamięć:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

tam możesz zobaczyć ip1i pobrać ip2adresy ii jiipp jeszcze nie istnieje. Nie zapominaj, że adresy to po prostu liczby całkowite zapisane w specjalnym typie.

Następnie deklarujesz i definiujesz ipptakie jak:

int **ipp = &ip1;

więc oto twoja pamięć:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

a następnie zmieniasz wartość wskazywaną przez przechowywany adres ipp, czyli adres przechowywany w ip1:

*ipp = ip2;

pamięć programu jest

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

NB: ponieważ int*jest to typ specjalny, wolę zawsze unikać deklarowania wielu wskaźników w tej samej linii, ponieważ myślę, że int *x;lubint *x, *y; notacja może wprowadzać w błąd. Wolę pisaćint* x; int* y;

HTH

zmo
źródło
ze swoim przykładzie, wartość początkowa ip2powinna być 3nie 4.
Dipto
1
och, właśnie zmieniłem pamięć, aby była zgodna z kolejnością deklaracji. Chyba naprawiłem to robiąc to?
zmo
5

Ponieważ kiedy mówisz

*ipp = ip2

mówisz „obiekt wskazany przez ipp”, aby wskazać kierunek pamięci, który ip2wskazuje.

Nie mówisz, ippżeby wskazywać ip2.

Diego R. Alcantara
źródło
4

Jeśli dodasz operator wyłuskiwania *do wskaźnika, przekierowujesz ze wskaźnika do wskazanego obiektu.

Przykłady:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

W związku z tym:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.
moooeeeep
źródło
3

Jeśli chcesz ippwskazać ip2, musisz powiedzieć ipp = &ip2;. Jednak to ip1nadal wskazywałoby i.

Andrejovich
źródło
3

Na samym początku ustawiłeś,

ipp = &ip1;

Teraz wyłuskuj to jako

*ipp = *&ip1 // Here *& becomes 1  
*ipp = ip1   // Hence proved 
Sunil Bojanapally
źródło
3

Rozważ każdą zmienną przedstawioną w ten sposób:

type  : (name, adress, value)

więc twoje zmienne powinny być reprezentowane w ten sposób

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

Ponieważ wartość ippjest &ip1taka instrukcja:

*ipp = ip2;

zmienia wartość na adresie &ip1na wartość z ip2, co oznacza ip1:

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

Ale ippnadal:

(ipp, &ipp, &ip1)

Więc wartość ippnadal &ip1oznacza, że ​​nadal wskazuje ip1.

rullof
źródło
1

Ponieważ zmieniasz wskaźnik *ipp. To znaczy

  1. ipp (nazwa zmienna) ---- wejdź do środka.
  2. wewnątrz ippjest adres ip1.
  3. teraz *ippidź do (adres od środka) ip1.

Teraz jesteśmy na ip1. *ipp(tj. ip1) = ip2.
ip2zawiera adres j.so ip1zawartość zostanie zastąpiona zawartością ip2 (tj. adres j), NIE ZMIENIAMY ippTREŚCI. OTÓŻ TO.

user3286725
źródło
1

*ipp = ip2; oznacza:

Przypisz ip2do zmiennej wskazywanej przez ipp. Więc jest to równoważne z:

ip1 = ip2;

Jeśli chcesz, aby adres ip2został zapisany ipp, po prostu wykonaj:

ipp = &ip2;

Teraz ippwskazuje na ip2.

Rikayan Bandyopadhyay
źródło
0

ippmoże przechowywać wartość (tj. wskazywać) wskaźnik do obiektu typu wskaźnik . Kiedy to zrobisz

ipp = &ip2;  

wtedy ippzawiera adres zmiennej (wskaźnik)ip2 , który jest ( &ip2) typu wskaźnik do wskaźnika . Teraz strzałka ippna drugim zdjęciu będzie wskazywać na ip2.

Encyklopedia mówi: operator operator nieprawidłowego działa na zmienną wskaźnika oraz zwraca l wartość (zmienna), co odpowiada wartości na wskaźnik adresu. Nazywa się to wyłuskiwaniem wskaźnika.
*

Stosowanie *operatora przy ippwyłuskiwaniu go do l-wartości wskaźnika doint typu. Wyłuskana wartość l *ippjest wskaźnikiemint typu do , może zawierać adres inttypu danych. Po oświadczeniu

ipp = &ip1;

ippposiada adres ip1i *ippposiada adres (wskazuje) i. Możesz powiedzieć, że *ippjest to alias ip1. Obie **ippi *ip1są aliasami dla i.
Wykonując

 *ipp = ip2;  

*ippi ip2oba wskazują na tę samą lokalizację, ale ippnadal wskazują ip1.

W *ipp = ip2;rzeczywistości kopiuje zawartość ip2(adres j) do ip1(jak *ippjest aliasem ip1), w efekcie wykonując oba wskaźniki ip1i ip2wskazując na ten sam obiekt ( j).
Tak więc, na drugim rysunku, strzałka ip1i ip2wskazuje, jpodczas gdy ippnadal wskazuje, ip1ponieważ nie jest wykonywana żadna modyfikacja w celu zmiany wartościipp .

haccks
źródło