Co oznacza „dereferencjonowanie” wskaźnika?


540

Podaj przykład z wyjaśnieniem.




24
int *p;zdefiniowałby wskaźnik do liczby całkowitej i *pwyreżyserowałby ten wskaźnik, co oznacza, że ​​faktycznie pobierałby dane, na które wskazuje p.
Peyman

4
Binky's Pointer Fun ( cslibrary.stanford.edu/104 ) to WIELKIE wideo o wskaźnikach, które mogą wyjaśnić różne rzeczy. @ Erik- Masz ochotę umieścić link do Biblioteki CS w Stanford. Jest tam tak wiele gadżetów ...
templatetypedef

6
Odpowiedź Harry'ego jest przeciwieństwem pomocnej tutaj.
Jim Balter

Odpowiedzi:


731

Przegląd podstawowej terminologii

To zwykle wystarczająco dobre - chyba że montaż programowania - wyobrazić sobie wskaźnik zawierający liczbowy adres pamięci, z 1 odnosząc się do drugiego bajtu w pamięci procesu, 2 trzecia, czwarta i 3 itd ....

  • Co się stało z 0 i pierwszym bajtem? Cóż, przejdziemy do tego później - patrz zerowe wskaźniki poniżej.
  • Aby uzyskać dokładniejszą definicję przechowywanych wskaźników oraz relacji między pamięcią a adresami, zobacz „Więcej informacji o adresach pamięci i dlaczego prawdopodobnie nie musisz wiedzieć” na końcu tej odpowiedzi.

Jeśli chcesz uzyskać dostęp do danych / wartości w pamięci, na którą wskazuje wskaźnik - zawartość adresu z tym indeksem liczbowym - wówczas odznaczasz wskaźnik.

Różne języki komputerowe mają różne oznaczenia, aby poinformować kompilator lub tłumacza, że ​​jesteś teraz zainteresowany wartością (bieżącą) wskazanego obiektu - poniżej skupiam się na C i C ++.

Scenariusz wskaźnikowy

Rozważ w C, biorąc pod uwagę wskaźnik taki jak pponiżej ...

const char* p = "abc";

... cztery bajty z wartościami liczbowymi użytymi do kodowania liter „a”, „b”, „c” oraz 0 bajtami oznaczającymi koniec danych tekstowych, są przechowywane gdzieś w pamięci i pod tym adresem numerycznym dane są przechowywane w p. W ten sposób C koduje tekst w pamięci znany jest jako ASCIIZ .

Na przykład, jeśli literał ciągu byłby pod adresem 0x1000, a p32-bitowy wskaźnik pod 0x2000, zawartość pamięci byłaby:

Memory Address (hex)    Variable name    Contents
1000                                     'a' == 97 (ASCII)
1001                                     'b' == 98
1002                                     'c' == 99
1003                                     0
...
2000-2003               p                1000 hex

Należy pamiętać, że nie istnieje zmienna nazwa / identyfikator adresu 0x1000, ale możemy pośrednio odnoszą się do łańcucha dosłowne stosując wskaźnik przechowującą adres: p.

Dereferencje wskaźnika

Aby odnieść się do znaków, pdo których odwołujemy się , odrzucamy pjedną z tych notacji (ponownie dla C):

assert(*p == 'a');  // The first character at address p will be 'a'
assert(p[1] == 'b'); // p[1] actually dereferences a pointer created by adding
                     // p and 1 times the size of the things to which p points:
                     // In this case they're char which are 1 byte in C...
assert(*(p + 1) == 'b');  // Another notation for p[1]

Możesz także przenosić wskaźniki przez wskazane dane, odsuwając je w trakcie:

++p;  // Increment p so it's now 0x1001
assert(*p == 'b');  // p == 0x1001 which is where the 'b' is...

Jeśli masz jakieś dane, które można zapisać, możesz wykonać następujące czynności:

int x = 2;
int* p_x = &x;  // Put the address of the x variable into the pointer p_x
*p_x = 4;       // Change the memory at the address in p_x to be 4
assert(x == 4); // Check x is now 4

Powyżej musisz wiedzieć w czasie kompilacji, że potrzebujesz zmiennej o nazwie x, a kod prosi kompilator, aby ustalił, gdzie powinien być przechowywany, zapewniając, że adres będzie dostępny za pośrednictwem &x.

Dereferencje i uzyskiwanie dostępu do elementu danych struktury

W C, jeśli masz zmienną będącą wskaźnikiem struktury z elementami danych, możesz uzyskać dostęp do tych członków za pomocą ->operatora dereferencji:

typedef struct X { int i_; double d_; } X;
X x;
X* p = &x;
p->d_ = 3.14159;  // Dereference and access data member x.d_
(*p).d_ *= -1;    // Another equivalent notation for accessing x.d_

Wielobajtowe typy danych

Aby użyć wskaźnika, program komputerowy potrzebuje również wglądu w rodzaj danych, na które jest wskazywany - jeśli ten typ danych wymaga więcej niż jednego bajtu do reprezentowania, wówczas wskaźnik zwykle wskazuje bajt o najniższym numerze w danych.

Patrząc na nieco bardziej złożony przykład:

double sizes[] = { 10.3, 13.4, 11.2, 19.4 };
double* p = sizes;
assert(p[0] == 10.3);  // Knows to look at all the bytes in the first double value
assert(p[1] == 13.4);  // Actually looks at bytes from address p + 1 * sizeof(double)
                       // (sizeof(double) is almost always eight bytes)
++p;                   // Advance p by sizeof(double)
assert(*p == 13.4);    // The double at memory beginning at address p has value 13.4
*(p + 2) = 29.8;       // Change sizes[3] from 19.4 to 29.8
                       // Note earlier ++p and + 2 here => sizes[3]

Wskaźniki do dynamicznie przydzielanej pamięci

Czasami nie wiesz, ile pamięci potrzebujesz, dopóki twój program nie uruchomi się i nie zobaczy, jakie dane są do niego rzucane ... wtedy możesz dynamicznie przydzielić pamięć malloc. Powszechną praktyką jest przechowywanie adresu we wskaźniku ...

int* p = (int*)malloc(sizeof(int)); // Get some memory somewhere...
*p = 10;            // Dereference the pointer to the memory, then write a value in
fn(*p);             // Call a function, passing it the value at address p
(*p) += 3;          // Change the value, adding 3 to it
free(p);            // Release the memory back to the heap allocation library

W C ++ alokacja pamięci jest zwykle wykonywana przez newoperatora, a zwalnianie za pomocą delete:

int* p = new int(10); // Memory for one int with initial value 10
delete p;

p = new int[10];      // Memory for ten ints with unspecified initial value
delete[] p;

p = new int[10]();    // Memory for ten ints that are value initialised (to 0)
delete[] p;

Zobacz także inteligentne wskaźniki C ++ poniżej.

Utrata i wyciek adresów

Często wskaźnik może być jedynym wskaźnikiem tego, gdzie w pamięci istnieją jakieś dane lub bufor. Jeśli potrzebne jest ciągłe korzystanie z tych danych / bufora lub możliwość wywołania free()lub deleteuniknięcia wycieku pamięci, programista musi działać na kopii wskaźnika ...

const char* p = asprintf("name: %s", name);  // Common but non-Standard printf-on-heap

// Replace non-printable characters with underscores....
for (const char* q = p; *q; ++q)
    if (!isprint(*q))
        *q = '_';

printf("%s\n", p); // Only q was modified
free(p);

... lub starannie zaplanuj cofnięcie wszelkich zmian ...

const size_t n = ...;
p += n;
...
p -= n;  // Restore earlier value...
free(p);

Inteligentne wskaźniki C ++

W C ++ najlepszą praktyką jest używanie inteligentnych obiektów wskaźnikowych do przechowywania wskaźników i zarządzania nimi, automatycznie zwalniając je po uruchomieniu niszczycieli inteligentnych wskaźników. Od wersji C ++ 11 biblioteka standardowa udostępnia dwa, unique_ptrna wypadek gdy dla przydzielonego obiektu istnieje jeden właściciel ...

{
    std::unique_ptr<T> p{new T(42, "meaning")};
    call_a_function(p);
    // The function above might throw, so delete here is unreliable, but...
} // p's destructor's guaranteed to run "here", calling delete

... i shared_ptrdo własności udziałów (z wykorzystaniem liczenia referencji ) ...

{
    auto p = std::make_shared<T>(3.14, "pi");
    number_storage1.may_add(p); // Might copy p into its container
    number_storage2.may_add(p); // Might copy p into its container    } // p's destructor will only delete the T if neither may_add copied it

Wskaźniki zerowe

W języku C NULLi 0- i dodatkowo w języku C ++ nullptr- można użyć do wskazania, że ​​wskaźnik nie posiada obecnie adresu pamięci zmiennej i nie powinien być wyzerowany ani używany w arytmetyce wskaźników. Na przykład:

const char* p_filename = NULL; // Or "= 0", or "= nullptr" in C++
int c;
while ((c = getopt(argc, argv, "f:")) != -1)
    switch (c) {
      case f: p_filename = optarg; break;
    }
if (p_filename)  // Only NULL converts to false
    ...   // Only get here if -f flag specified

W C i C ++, podobnie jak wbudowane typy liczbowe niekoniecznie są domyślnie ustawione na 0, ani boolsna false, wskaźniki nie zawsze są ustawione na NULL. Wszystkie te są ustawione na 0 / false / NULL, gdy są to staticzmienne lub (tylko C ++) bezpośrednie lub pośrednie zmienne składowe obiektów statycznych lub ich zasad, lub podlegają zerowej inicjalizacji (np. new T();I new T(x, y, z);przeprowadzają zerową inicjalizację elementów T, w tym wskaźników, podczas gdy new T;nie).

Ponadto, gdy można przypisać 0, NULLa nullptrdo wskaźnika bity we wskaźniku niekoniecznie wszystko resetu: wskaźnik nie może zawierać „0” na poziomie sprzętowym, lub skierować do adresu 0 w wirtualnej przestrzeni adresowej. Kompilator może tam coś innego sklepu, jeśli ma powody, ale co robi - jeśli przyjść i porównać wskaźnik do 0, NULL, nullptrlub inny wskaźnik, który został przypisany do żadnego z powyższych prac porównania muszą zgodnie z oczekiwaniami. Zatem poniżej kodu źródłowego na poziomie kompilatora „NULL” jest potencjalnie nieco „magiczny” w językach C i C ++ ...

Więcej informacji na temat adresów pamięci i powodów, dla których prawdopodobnie nie musisz tego wiedzieć

Mówiąc ściślej, zainicjowane wskaźniki przechowują wzór bitowy identyfikujący albo NULL(często wirtualny ) adres pamięci.

Prosty przypadek polega na tym, że jest to numeryczne przesunięcie w całej wirtualnej przestrzeni adresowej procesu; w bardziej złożonych przypadkach wskaźnik może odnosić się do określonego obszaru pamięci, który procesor może wybrać na podstawie rejestrów „segmentu” procesora lub innego rodzaju identyfikatora segmentu zakodowanego we wzorcu bitowym i / lub szukać w różnych miejscach w zależności od instrukcje kodu maszynowego przy użyciu adresu.

Na przykład int*poprawnie zainicjowany, aby wskazywał intzmienną, może - po rzutowaniu do float*- dostępu do pamięci w pamięci „GPU” zupełnie różnić się od pamięci, w której intznajduje się zmienna, a następnie po rzutowaniu do i użyciu jako wskaźnik funkcji może wskazywać na dalsze odrębne kody maszyn przechowujących pamięć dla programu (z wartością liczbową int*efektywnie losowego, niepoprawnego wskaźnika w tych innych obszarach pamięci).

Języki programowania 3GL, takie jak C i C ++, zwykle ukrywają tę złożoność, na przykład:

  • Jeśli kompilator daje Ci wskaźnik do zmiennej lub funkcji, możesz swobodnie wyrejestrować ją (pod warunkiem, że zmienna nie zostanie w międzyczasie zniszczona / cofnięta) i problem kompilatora polega na tym, czy np. Należy wcześniej przywrócić konkretny rejestr segmentu procesora, czy też użyta odrębna instrukcja kodu maszynowego

  • Jeśli otrzymasz wskaźnik do elementu w tablicy, możesz użyć arytmetyki wskaźnika, aby przenieść się w dowolne miejsce w tablicy, a nawet utworzyć adres znajdujący się za końcem tablicy, który można porównać z innymi wskaźnikami do elementów w tablicy (lub które zostały podobnie przesunięte przez arytmetykę wskaźnika do tej samej wartości jeden za końcem); ponownie w C i C ++, od kompilatora zależy, czy to „po prostu działa”

  • Określone funkcje systemu operacyjnego, np. Mapowanie pamięci współużytkowanej, mogą dać wskazówki, a one „po prostu będą działać” w zakresie adresów, który ma dla nich sens

  • Próby przeniesienia legalnych wskaźników poza te granice lub rzucenia dowolnych liczb na wskaźniki lub użycia wskaźników rzutowanych na niepowiązane typy, zwykle mają niezdefiniowane zachowanie , więc należy tego unikać w bibliotekach i aplikacjach wyższego poziomu, ale kod dla systemów operacyjnych, sterowników urządzeń itp. Może być konieczne poleganie na zachowaniu niezdefiniowanym przez standard C lub C ++, który jest jednak dobrze określony przez ich konkretną implementację lub sprzęt.


jest p[1] i *(p + 1) identyczny ? To znaczy: Czy p[1] i *(p + 1)generuje te same instrukcje?
Pacerier

2
@Pacerier: od 6.5.2.1/2 w normie C projektu N1570 (pierwszy znalazłem online) „Definicja operatora indeksu dolnego [] jest taka, że ​​E1 [E2] jest identyczne z (* ((E1) + (E2)) ). ” - Nie mogę sobie wyobrazić żadnego powodu, dla którego kompilator nie od razu przekonwertowałby ich na identyczne reprezentacje we wczesnym etapie kompilacji, stosując później te same optymalizacje, ale nie rozumiem, w jaki sposób ktoś może zdecydowanie udowodnić, że kod byłby identyczny bez sprawdzania każdego napisanego kompilatora.
Tony Delroy,

3
@ Kochanie: wartość 1000 hex jest zbyt duża, aby zakodować ją w jednym bajcie (8 bitów) pamięci: w jednym bajcie można przechowywać tylko liczby bez znaku od 0 do 255. Tak więc po prostu nie można zapisać 1000 znaków szesnastkowych pod „tylko” adresem 2000. Zamiast tego system 32-bitowy używałby 32 bitów - co oznacza cztery bajty - z adresami od 2000 do 2003. System 64-bitowy użyłby 64 bity - 8 bajtów - od 2000 do 2007. Tak czy inaczej, adres bazowy pto tylko 2000: gdybyś miał inny wskaźnik p, musiałby zapisać 2000 w swoich czterech lub ośmiu bajtach. Mam nadzieję, że to pomaga! Twoje zdrowie.
Tony Delroy

1
@TonyDelroy: Jeśli związek uzawiera tablicę arr, zarówno gcc, jak i clang rozpoznają, że wartość u.arr[i]może uzyskać dostęp do tego samego magazynu, co inni członkowie związku, ale nie rozpozna, że ​​wartość *(u.arr+i)może to zrobić. Nie jestem pewien, czy autorzy tych kompilatorów uważają, że ten ostatni wywołuje UB, czy też pierwszy wywołuje UB, ale i tak powinni go z powodzeniem przetworzyć, ale wyraźnie widzą te dwa wyrażenia jako różne.
supercat

3
Rzadko widywałem wskaźniki i ich użycie w C / C ++ tak zwięźle i prosto wyjaśnione.
kayleeFrye_onDeck

102

Dereferencjowanie wskaźnika oznacza uzyskanie wartości przechowywanej w miejscu pamięci wskazywanym przez wskaźnik. Do tego służy operator *, który nazywa się operatorem dereferencyjnym.

int a = 10;
int* ptr = &a;

printf("%d", *ptr); // With *ptr I'm dereferencing the pointer. 
                    // Which means, I am asking the value pointed at by the pointer.
                    // ptr is pointing to the location in memory of the variable a.
                    // In a's location, we have 10. So, dereferencing gives this value.

// Since we have indirect control over a's location, we can modify its content using the pointer. This is an indirect way to access a.

 *ptr = 20;         // Now a's content is no longer 10, and has been modified to 20.

15
Wskaźnik nie wskazuje wartości , wskazuje obiekt .
Keith Thompson

51
@KeithThompson Wskaźnik nie wskazuje obiektu, wskazuje adres pamięci, w którym znajduje się obiekt (być może prymityw).
mg30rg

4
@ mg30rg: Nie jestem pewien, co czynisz rozróżnieniem. Wartość wskaźnika to adres. Obiekt z definicji jest „regionem przechowywania danych w środowisku wykonawczym, którego zawartość może reprezentować wartości”. A co rozumiesz przez „prymitywne”? Standard C nie używa tego terminu.
Keith Thompson

6
@KeithThompson Ledwo zauważyłem, że tak naprawdę nie dodałeś wartości do odpowiedzi, tylko naciągałeś terminologię (i zrobiłeś to również źle). Wartość wskaźnika na pewno jest adresem, w ten sposób „wskazuje” na adres pamięci. Słowo „obiekt” w naszym świecie OOPdriven może wprowadzać w błąd, ponieważ może być interpretowane jako „instancja klasy” (tak, nie wiedziałem, że pytanie jest oznaczone jako [C], a nie [C ++]), i użyłem tego słowa „prymitywne” jak w przeciwieństwo „copmlex” (struktura danych jak struktura lub klasa).
mg30rg

3
Dodaję do tej odpowiedzi, że operator indeksu tablicy []również odznacza wskaźnik ( a[b]jest zdefiniowany jako oznaczający *(a + b)).
cmaster

20

Wskaźnik jest „odniesieniem” do wartości. Podobnie jak numer wywoławczy w bibliotece jest odniesieniem do książki. „Dereferencje” numer telefonu fizycznie przechodzi i pobiera tę książkę.

int a=4 ;
int *pA = &a ;
printf( "The REFERENCE/call number for the variable `a` is %p\n", pA ) ;

// The * causes pA to DEREFERENCE...  `a` via "callnumber" `pA`.
printf( "%d\n", *pA ) ; // prints 4.. 

Jeśli nie ma tej książki, bibliotekarz zaczyna krzyczeć, zamyka bibliotekę, a kilka osób postanawia zbadać przyczynę znalezienia książki, której nie ma.


18

Krótko mówiąc, dereferencje oznaczają dostęp do wartości z określonego miejsca w pamięci, na które wskazuje ten wskaźnik.


7

Kod i objaśnienia z Podstawy wskaźnika :

Operacja dereferencji rozpoczyna się od wskaźnika i podąża za strzałką, aby uzyskać dostęp do punktu. Celem może być sprawdzenie stanu pointee lub zmiana stanu pointee. Operacja dereferencji na wskaźniku działa tylko wtedy, gdy wskaźnik ma pointee - pointee musi zostać przydzielony, a wskaźnik musi być ustawiony tak, aby wskazywał na niego. Najczęstszym błędem w kodzie wskaźnika jest zapomnienie o konfiguracji pointee. Najczęstszą awarią środowiska wykonawczego z powodu tego błędu w kodzie jest nieudana operacja dereferencji. W Javie niepoprawne dereferencje zostaną oznaczone przez system wykonawczy grzecznie. W skompilowanych językach, takich jak C, C ++ i Pascal, nieprawidłowe dereferencje czasami się zawieszają, a innym razem psują pamięć w subtelny, losowy sposób.

void main() {   
    int*    x;  // Allocate the pointer x
    x = malloc(sizeof(int));    // Allocate an int pointee,
                            // and set x to point to it
    *x = 42;    // Dereference x to store 42 in its pointee   
}

Musisz przydzielić pamięć, na którą ma wskazywać x. Twój przykład ma nieokreślone zachowanie.
Peyman

3

Myślę, że wszystkie poprzednie odpowiedzi są błędne, ponieważ stwierdzają, że dereferencje oznaczają dostęp do rzeczywistej wartości. Zamiast tego Wikipedia podaje poprawną definicję: https://en.wikipedia.org/wiki/Dereference_operator

Działa na zmiennej wskaźnika i zwraca wartość l równoważną wartości pod adresem wskaźnika. Nazywa się to „dereferencją” wskaźnika.

To powiedziawszy, możemy wyrejestrować wskaźnik bez dostępu do wartości, na którą wskazuje. Na przykład:

char *p = NULL;
*p;

Wyrejestrowaliśmy wskaźnik NULL bez dostępu do jego wartości. Lub możemy zrobić:

p1 = &(*p);
sz = sizeof(*p);

Ponownie, dereferencje, ale nigdy nie uzyskując dostępu do wartości. Taki kod NIE ulega awarii: Awaria występuje, gdy faktycznie uzyskujesz dostęp do danych za pomocą nieprawidłowego wskaźnika. Jednak, niestety, zgodnie ze standardem, dereferencjowanie nieprawidłowego wskaźnika jest niezdefiniowanym zachowaniem (z kilkoma wyjątkami), nawet jeśli nie spróbujesz dotknąć rzeczywistych danych.

W skrócie: dereferencja wskaźnika oznacza zastosowanie do niego operatora dereferencji. Ten operator po prostu zwraca wartość l do przyszłego wykorzystania.


cóż, wyrejestrowałeś wskaźnik NULL, co doprowadziłoby do błędu segmentacji.
arjun gaur

poza tym szukałeś „operatora dereferencji”, a nie „dereferencji wskaźnika”, co w rzeczywistości oznacza uzyskanie wartości / dostęp do wartości w miejscu pamięci wskazywanym przez wskaźnik.
arjun gaur

Czy próbowałeś? Zrobiłem. Następujące polecenie nie ulega awarii: `#include <stdlib.h> int main () {char * p = NULL; * p; zwraca 0; } `
stsp

1
@stsp Robi, ponieważ kod nie ulega teraz awarii nie oznacza, że ​​nie będzie w przyszłości ani w innym systemie.

1
*p;powoduje niezdefiniowane zachowanie. Chociaż masz rację, że nie ma dostępu do wyłuskania wartość per se , kod *p; ma dostęp do wartości.
MM
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.