Jak działa wskaźnik do wskaźników w C?


171

Jak działają wskaźniki do wskaźników w C? Kiedy ich użyjesz?


43
Nie, nie praca domowa ... po prostu chciałem wiedzieć ... bo często to widzę, kiedy czytam kod C.

1
Wskaźnik do wskaźnika nie jest szczególnym przypadkiem czegoś, więc nie rozumiem, czego nie rozumiesz na temat void **.
akappa

dla tablic 2D najlepszym przykładem są argumenty wiersza poleceń „prog arg1 arg2” są przechowywane jako char ** argv. A jeśli rozmówca robi chcą przydzielać pamięć (wywoływana funkcja alokacji pamięci)
resultsway

1
Masz ładny przykład użycia „wskaźnika do wskaźnika” w Git 2.0: zobacz moją odpowiedź poniżej
VonC

Odpowiedzi:


359

Załóżmy, że mamy 8-bitowy komputer z 8-bitowymi adresami (a więc tylko 256 bajtów pamięci). To jest część tej pamięci (liczby na górze to adresy):

  54   55   56   57   58   59   60   61   62   63   64   65   66   67   68   69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
|    | 58 |    |    | 63 |    | 55 |    |    | h  | e  | l  | l  | o  | \0 |    |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+

Widać tutaj, że pod adresem 63 zaczyna się napis „hello”. W tym przypadku, jeśli jest to jedyne wystąpienie „cześć” w pamięci,

const char *c = "hello";

... definiuje csię jako wskaźnik do (tylko do odczytu) ciągu znaków „hello”, a zatem zawiera wartość 63. cSama musi być gdzieś przechowywana: w powyższym przykładzie w lokalizacji 58. Oczywiście możemy nie tylko wskazywać na znaki , ale także do innych wskazówek. Na przykład:

const char **cp = &c;

Teraz cpwskazuje na c, to znaczy zawiera adres c(czyli 58). Możemy pójść jeszcze dalej. Rozważać:

const char ***cpp = &cp;

Teraz cppprzechowuje adres cp. Ma więc wartość 55 (na podstawie powyższego przykładu) i zgadłeś: sam jest przechowywany pod adresem 60.


Co do tego, dlaczego używa się wskaźników do wskaźników:

  • Nazwa tablicy zwykle zawiera adres jej pierwszego elementu. Więc jeśli tablica zawiera elementy typu t, odwołanie do tablicy ma typ t *. Rozważmy teraz tablicę tablic typu t: naturalnie odniesienie do tej tablicy 2D będzie miało typ (t *)*= t **, a zatem jest wskaźnikiem do wskaźnika.
  • Chociaż tablica ciągów brzmi jednowymiarowo, w rzeczywistości jest dwuwymiarowa, ponieważ łańcuchy są tablicami znaków. Stąd: char **.
  • Funkcja fbędzie musiała zaakceptować argument typu, t **jeśli ma zmienić zmienną typu t *.
  • Wiele innych powodów, które są zbyt liczne, aby je tutaj wymienić.

7
tak dobry przykład… rozumiem, czym one są… ale ważniejsze jest, jak i kiedy ich używać… teraz…

2
Stephan wykonał dobrą robotę, odtwarzając zasadniczo diagram w języku programowania C. Kernighan & Richie. Jeśli programujesz w C i nie masz tej książki i nie przeszkadza ci papierowa dokumentacja, zdecydowanie sugeruję, żebyś ją zdobył, (dość) skromny wydatek zwróci się bardzo szybko w wydajności. Zwykle jest to bardzo jasne w jego przykładach.
J. Polfer

4
char * c = "hello" powinno być const char * c = "hello". Również mylące jest stwierdzenie, że „tablica jest przechowywana jako adres pierwszego elementu”. Tablica jest przechowywana jako ... tablica. Często jego nazwa zawiera wskaźnik do pierwszego elementu, ale nie zawsze. Jeśli chodzi o wskaźniki do wskaźników, powiedziałbym po prostu, że są one przydatne, gdy funkcja musi zmodyfikować wskaźnik przekazany jako parametr (zamiast tego przekazuje się wskaźnik do wskaźnika).
Bastien Léonard

4
O ile nie zinterpretuję tej odpowiedzi źle, wygląda to źle. c jest przechowywane w 58 i wskazuje 63, cp jest przechowywane w 55 i wskazuje 58, a cpp nie jest przedstawione na schemacie.
Thanatos

1
Wygląda dobrze. Tylko drobny problem powstrzymywał mnie od powiedzenia: Świetny post. Samo wyjaśnienie było doskonałe. Zmiana na głos za pozytywną. (Może stackoverflow potrzebuje przeglądu wskaźników?)
Thanatos

46

Jak działają wskaźniki do wskaźników w C?

Po pierwsze, wskaźnik jest zmienną, jak każda inna zmienna, ale zawiera adres zmiennej.

Wskaźnik do wskaźnika jest zmienną, podobnie jak każda inna zmienna, ale zawiera adres zmiennej. Ta zmienna jest po prostu wskaźnikiem.

Kiedy ich użyjesz?

Możesz ich użyć, gdy chcesz zwrócić wskaźnik do jakiejś pamięci na stercie, ale nie używasz zwracanej wartości.

Przykład:

int getValueOf5(int *p)
{
  *p = 5;
  return 1;//success
}

int get1024HeapMemory(int **p)
{
  *p = malloc(1024);
  if(*p == 0)
    return -1;//error
  else 
    return 0;//success
}

I nazywasz to tak:

int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5

int *p;    
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap

Istnieją również inne zastosowania, na przykład argument main () każdego programu w języku C ma wskaźnik do wskaźnika dla argv, gdzie każdy element zawiera tablicę znaków, które są opcjami wiersza poleceń. Musisz jednak zachować ostrożność, gdy używasz wskaźników lub wskaźników do wskazywania dwuwymiarowych tablic, lepiej jest zamiast tego użyć wskaźnika do dwuwymiarowej tablicy.

Dlaczego to jest niebezpieczne?

void test()
{
  double **a;
  int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)

  double matrix[ROWS][COLUMNS];
  int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}

Oto przykład prawidłowo wykonanego wskaźnika do dwuwymiarowej tablicy:

int (*myPointerTo2DimArray)[ROWS][COLUMNS]

Nie możesz jednak użyć wskaźnika do dwuwymiarowej tablicy, jeśli chcesz obsługiwać zmienną liczbę elementów dla ROWS i COLUMNS. Ale kiedy już wiesz, użyjesz tablicy dwuwymiarowej.


32

Podoba mi się ten "prawdziwy świat" przykład użycia wskaźnika do wskaźnika, w Git 2.0, zatwierdzenie 7b1004b :

Linus powiedział kiedyś:

Właściwie chciałbym, żeby więcej ludzi zrozumiało podstawowy rodzaj kodowania niskiego poziomu. Niewielkie, skomplikowane rzeczy, takie jak wyszukiwanie nazw bez blokad, ale po prostu dobre wykorzystanie wskaźników do wskaźników itp.
Na przykład widziałem zbyt wiele osób, które usuwały wpis z listy z pojedynczym łączem, śledząc wpis „prev” , a następnie usunąć wpis, robiąc coś w rodzaju

if (prev)
  prev->next = entry->next;
else
  list_head = entry->next;

a kiedy widzę taki kod, po prostu mówię „Ta osoba nie rozumie wskaźników”. I niestety jest to dość powszechne.

Ludzie, którzy rozumieją wskaźniki, po prostu używają „ wskaźnika do wskaźnika wpisu ” i inicjują go adresem list_head. A potem, gdy przeglądają listę, mogą usunąć wpis bez używania jakichkolwiek warunków, po prostu wykonując polecenie

*pp =  entry->next

http://i.stack.imgur.com/bpfxT.gif

Zastosowanie tego uproszczenia pozwala stracić 7 wierszy z tej funkcji nawet podczas dodawania 2 wierszy komentarza.

-   struct combine_diff_path *p, *pprev, *ptmp;
+   struct combine_diff_path *p, **tail = &curr;

Chris zwraca uwagę w komentarzach do wideo z 2016 roku „ Linus Torvalds's Double Pointer Problem ” autorstwa Philipa Buucka .


kumar zwraca uwagę w komentarzach do wpisu na blogu „ Linus on Understanding Pointers ”, w którym Grisha Trubetskoy wyjaśnia:

Wyobraź sobie, że masz połączoną listę zdefiniowaną jako:

typedef struct list_entry {
    int val;
    struct list_entry *next;
} list_entry;

Musisz iterować go od początku do końca i usunąć określony element, którego wartość jest równa wartości to_remove.
Bardziej oczywistym sposobem byłoby:

list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;

while (entry) { /* line 4 */
    if (entry->val == to_remove)     /* this is the one to remove ; line 5 */
        if (prev)
           prev->next = entry->next; /* remove the entry ; line 7 */
        else
            head = entry->next;      /* special case - first entry ; line 9 */

    /* move on to the next entry */
    prev = entry;
    entry = entry->next;
}

To, co robimy powyżej, to:

  • iterowanie po liście, aż pozycja jest NULL, co oznacza, że ​​dotarliśmy do końca listy (linia 4).
  • Kiedy natrafimy na wpis, który chcemy usunąć (wiersz 5),
    • przypisujemy wartość bieżącego następnego wskaźnika do poprzedniego,
    • eliminując w ten sposób aktualny element (wiersz 7).

Powyżej występuje specjalny przypadek - na początku iteracji nie ma poprzedniego wpisu ( prevjest NULL), więc aby usunąć pierwszy wpis z listy, należy zmodyfikować samą nagłówek (wiersz 9).

Linus mówił, że powyższy kod można uprościć, czyniąc z poprzedniego elementu wskaźnik do wskaźnika, a nie tylko wskaźnik .
Kod wygląda wtedy następująco:

list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;

while (entry) {
    if (entry->val == to_remove)
        *pp = entry->next;

    pp = &entry->next;
    entry = entry->next;
}

Powyższy kod jest bardzo podobny do poprzedniego wariantu, ale zwróć uwagę, że nie musimy już zwracać uwagi na specjalny przypadek pierwszego elementu listy, ponieważ ppnie jest on NULLna początku. Prosty i sprytny.

Ponadto ktoś w tym wątku skomentował, że powodem jest to, że *pp = entry->nextjest lepsze, ponieważ jest atomowe. Z pewnością NIE jest atomowy .
Powyższe wyrażenie zawiera dwa operatory wyłuskiwania ( *i ->) oraz jedno przypisanie, a żadna z tych trzech rzeczy nie jest atomowa.
Jest to powszechne nieporozumienie, ale niestety prawie nic w C nie powinno być nigdy uznawane za niepodzielne (łącznie z operatorami ++i --)!



@kumar dobre odniesienie. zawarłem to w odpowiedzi dla większej widoczności.
VonC

Ten film był dla mnie niezbędny do zrozumienia twojego przykładu. W szczególności czułem się zdezorientowany (i wojowniczy), dopóki nie narysowałem diagramu pamięci i nie prześledziłem postępu programu. To powiedziawszy, nadal wydaje mi się nieco tajemnicze.
Chris,

@Chris Świetny film, dziękuję za wspomnienie! W odpowiedzi zawarłem Twój komentarz, aby uzyskać lepszą widoczność.
VonC,

14

Omawiając wskazówki na kursie programowania na uniwersytecie, otrzymaliśmy dwie wskazówki, jak zacząć się ich uczyć. Pierwszym było obejrzenie Pointer Fun With Binky . Drugą była myśl o przejściu Oczy plamiaka z książki Lewisa Carrolla Through the Looking-Glass

„Jesteś smutny”, powiedział Rycerz z niepokojem: „Pozwól, że zaśpiewam ci piosenkę, aby cię pocieszyć”.

„Czy to jest bardzo długie?” - spytała Alice, bo tego dnia słyszała sporo poezji.

„Jest długi”, powiedział Rycerz, „ale jest bardzo, bardzo piękny. Każdy, kto słyszy, jak ją śpiewam - albo doprowadza to łzy do oczu, albo ...

„Albo co jeszcze?” - powiedziała Alicja, ponieważ Rycerz zrobił nagłą przerwę.

- W przeciwnym razie nie, wiesz. Tytuł piosenki to „oczy plamiaka”. ”

„Och, to jest tytuł piosenki, prawda?” Powiedziała Alice, starając się być zainteresowana.

- Nie, nie rozumiesz - powiedział Rycerz, wyglądając na lekko zirytowanego. „Tak nazywa się ta nazwa. Naprawdę nazywam się „The Aged Aged Man”.

„W takim razie powinienem był powiedzieć 'Tak nazywa się ta piosenka'? Alice poprawiła się.

„Nie, nie powinieneś: to zupełnie inna sprawa! Piosenka nosi tytuł „Ways And Means”: ale tak się nazywa, wiesz! ”

„Więc co to za piosenka?” - powiedziała Alicja, która do tego czasu była całkowicie oszołomiona.

- Właśnie do tego doszedłem - powiedział Rycerz. „Piosenka naprawdę jest 'A-sitting On A Gate': a melodia jest moim własnym wynalazkiem.”


1
Musiałem przeczytać ten fragment kilka razy ... +1 za skłonienie mnie do myślenia!
Ruben Steins

Dlatego Lewis Carroll nie jest zwykłym pisarzem.
metarose

1
Więc ... to tak by wyglądało? name -> 'The Aged Aged Man' -> o nazwie -> 'Haddock's Eyes' -> piosenka -> 'A-sitting On A Gate'
tisaconundrum


7

Gdy wymagane jest odniesienie do wskaźnika. Na przykład, gdy chcesz zmodyfikować wartość (wskazany adres) zmiennej wskaźnikowej zadeklarowanej w zakresie funkcji wywołującej wewnątrz wywoływanej funkcji.

Jeśli przekażesz pojedynczy wskaźnik jako argument, zmodyfikujesz lokalne kopie wskaźnika, a nie oryginalny wskaźnik w zakresie wywołującym. Za pomocą wskaźnika do wskaźnika modyfikujesz ten ostatni.


Dobrze wyjaśnione w części „Dlaczego”
Rana Deep

7

Wskaźnik do wskaźnika jest również nazywany uchwytem . Jednym z zastosowań tego jest często sytuacja, gdy obiekt można przenieść w pamięci lub usunąć. Często jest się odpowiedzialnym za zablokowanie i odblokowanie możliwości korzystania z obiektu, aby nie był on poruszany podczas uzyskiwania do niego dostępu.

Jest często używany w środowisku o ograniczonej pamięci, np. Palm OS.

computer.howstuffworks.com Link >>

www.flippinbits.com Link >>


7

Rozważ poniższy rysunek i program, aby lepiej zrozumieć tę koncepcję .

Diagram podwójnego wskaźnika

Jak na rysunku, ptr1 jest pojedynczym wskaźnikiem, który ma adres zmiennej num .

ptr1 = #

Podobnie ptr2 jest wskaźnikiem do wskaźnika (podwójnym wskaźnikiem), który ma adres wskaźnika ptr1 .

ptr2 = &ptr1;

Wskaźnik wskazujący na inny wskaźnik jest nazywany podwójnym wskaźnikiem. W tym przykładzie ptr2 jest podwójnym wskaźnikiem.

Wartości z powyższego wykresu:

Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000

Przykład:

#include <stdio.h>

int main ()
{
   int  num = 10;
   int  *ptr1;
   int  **ptr2;

   // Take the address of var 
   ptr1 = &num;

   // Take the address of ptr1 using address of operator &
   ptr2 = &ptr1;

   // Print the value
   printf("Value of num = %d\n", num );
   printf("Value available at *ptr1 = %d\n", *ptr1 );
   printf("Value available at **ptr2 = %d\n", **ptr2);
}

Wynik:

Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10

5

jest to wskaźnik do wartości adresu wskaźnika. (to okropne, wiem)

w zasadzie umożliwia przekazanie wskaźnika do wartości adresu innego wskaźnika, dzięki czemu można modyfikować miejsce, w którym inny wskaźnik wskazuje z funkcji podrzędnej, na przykład:

void changeptr(int** pp)
{
  *pp=&someval;
}

przepraszam, wiem, że to było bardzo złe. Spróbuj przeczytać, erm, to: codeproject.com/KB/cpp/PtrToPtr.aspx
Luke Schafer

5

Masz zmienną, która zawiera adres. To jest wskaźnik.

Następnie masz inną zmienną, która zawiera adres pierwszej zmiennej. To jest wskaźnik do wskaźnika.


3

Wskaźnik do wskaźnika jest, cóż, wskaźnikiem do wskaźnika.

Sensownym przykładem jakiegoś typu ** jest tablica dwuwymiarowa: masz jedną tablicę wypełnioną wskaźnikami do innych tablic, więc kiedy piszesz

dpointer [5] [6]

uzyskujesz dostęp do tablicy, która zawiera wskaźniki do innych tablic na jego piątej pozycji, uzyskujesz wskaźnik (niech fpointer jego nazwę), a następnie uzyskujesz dostęp do szóstego elementu tablicy, do której odwołuje się ta tablica (czyli fpointer [6]).


2
wskaźników do wskaźników nie należy mylić z tablicami rank2, np. int x [10] [10], gdzie piszesz x [5] [6], uzyskujesz dostęp do wartości w tablicy.
Pete Kirkham

To tylko przykład, w którym nieważne ** jest odpowiednie. Wskaźnik do wskaźnika to tylko wskaźnik, który wskazuje na wskaźnik.
akappa

1

Jak to działa: jest to zmienna, która może przechowywać inny wskaźnik.

Kiedy ich użyjesz: Wiele z nich używa jednego z nich, jeśli twoja funkcja chce skonstruować tablicę i zwrócić ją do obiektu wywołującego.

//returns the array of roll nos {11, 12} through paramater
// return value is total number of  students
int fun( int **i )
{
    int *j;
    *i = (int*)malloc ( 2*sizeof(int) );
    **i = 11;  // e.g., newly allocated memory 0x2000 store 11
    j = *i;
    j++;
    *j = 12; ;  // e.g., newly allocated memory 0x2004 store 12

    return 2;
}

int main()
{
    int *i;
    int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
    for ( int j=0; j<n; j++ )
        printf( "roll no = %d \n", i[j] );

    return 0;
}


0

Jest tak wiele przydatnych wyjaśnień, ale nie znalazłem tylko krótkiego opisu, więc ...

Zasadniczo wskaźnik to adres zmiennej. Krótki kod podsumowujący:

     int a, *p_a;//declaration of normal variable and int pointer variable
     a = 56;     //simply assign value
     p_a = &a;   //save address of "a" to pointer variable
     *p_a = 15;  //override the value of the variable

//print 0xfoo and 15 
//- first is address, 2nd is value stored at this address (that is called dereference)
     printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a); 

Również przydatne informacje można znaleźć w temacie Co oznacza odniesienie i wyłuskiwanie

I nie jestem taki pewien, kiedy wskaźniki mogą być przydatne, ale powszechnie jest to konieczne, gdy robisz jakąś ręczną / dynamiczną alokację pamięci - malloc, calloc itp.

Mam więc nadzieję, że pomoże to również w wyjaśnieniu problemu :)

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.