Jak działają wskaźniki do wskaźników w C? Kiedy ich użyjesz?
Jak działają wskaźniki do wskaźników w C? Kiedy ich użyjesz?
Odpowiedzi:
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:
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.char **.fbędzie musiała zaakceptować argument typu, t **jeśli ma zmienić zmienną typu t *.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.
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

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 (
prevjestNULL), 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 onNULLna 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--)!
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.”
Możesz przeczytać to: Wskaźniki do wskaźników
Mam nadzieję, że pomoże to wyjaśnić kilka podstawowych wątpliwości.
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.
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.
Rozważ poniższy rysunek i program, aby lepiej zrozumieć tę koncepcję .
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 = #
// 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
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;
}
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]).
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;
}
Stworzyłem 5-minutowy film, który wyjaśnia, jak działają wskaźniki:
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 :)