Ta lekcja jest opcjonalna i przeznaczona dla zaawansowanych czytelników, którzy chcą dowiedzieć się więcej o C++. Żadne przyszłe lekcje nie będą opierać się na tej lekcji.
Wskaźnik na wskaźnik jest dokładnie tym, czego można się spodziewać: wskaźnikiem przechowującym adres innego wskaźnika.
Wskaźniki na wskaźniki
Zwykły wskaźnik na int jest deklarowany za pomocą pojedynczej gwiazdki:
int* ptr; // wskaźnik na int, jedna gwiazdkaWskaźnik na wskaźnik na int jest deklarowany za pomocą dwóch gwiazdki
int** ptrptr; // wskaźnik na wskaźnik na int, dwie gwiazdkiWskaźnik do wskaźnika działa tak samo jak zwykły wskaźnik — można go wyłuskać, aby pobrać wskazywaną wartość. A ponieważ ta wartość sama w sobie jest wskaźnikiem, możesz ją ponownie odwołać, aby dostać się do wartości bazowej. Te dereferencje można wykonać kolejno:
int value { 5 };
int* ptr { &value };
std::cout << *ptr << '\n'; // Wyzeruj wskaźnik do int, aby uzyskać wartość int
int** ptrptr { &ptr };
std::cout << **ptrptr << '\n'; // dereferencja, aby uzyskać wskaźnik do int, ponownie wyłuskaj, aby uzyskać wartość intPowyższy program wypisuje:
5 5
Zauważ, że nie możesz ustawić wskaźnika na wskaźnik bezpośrednio na wartość:
int value { 5 };
int** ptrptr { &&value }; // nieprawidłowaDzieje się tak, ponieważ adres operatora (operator&) wymaga lwartości, ale &wartość jest rwartością.
Jednakże wskaźnik do wskaźnika może mieć wartość null:
int** ptrptr { nullptr };Tablice wskaźniki
Wskaźniki na wskaźniki mają kilka zastosowań. Najbardziej powszechnym zastosowaniem jest dynamiczne przydzielanie tablicy wskaźników:
int** array { new int*[10] }; // przydziel tablicę 10 wskaźników intDziała to podobnie jak standardowa tablica alokowana dynamicznie, z tą różnicą, że elementy tablicy są typu „wskaźnik do liczby całkowitej”, a nie liczby całkowitej.
Dwuwymiarowe tablice alokowane dynamicznie
Innym powszechnym zastosowaniem wskaźników do wskaźników jest umożliwienie dynamicznie alokowanych tablic wielowymiarowych (zobacz 17.12 — Wielowymiarowe tablice w stylu C dla przegląd tablic wielowymiarowych).
W przeciwieństwie do dwuwymiarowej stałej tablicy, którą można łatwo zadeklarować w ten sposób:
int array[10][5];Dynamiczna alokacja tablicy dwuwymiarowej jest nieco trudniejsza. Możesz ulec pokusie, aby spróbować czegoś takiego:
int** array { new int[10][5] }; // won’t work!Ale to nie zadziała.
Są dwa możliwe rozwiązania. Jeśli skrajnym prawym wymiarem tablicy jest constexpr, możesz to zrobić w następujący sposób:
int x { 7 }; // non-constant
int (*array)[5] { new int[x][5] }; // najbardziej prawy wymiar musi być constexprNawiasy są wymagane, aby kompilator wiedział, że chcemy array być wskaźnikiem do tablicy zawierającej 5 int (która w tym przypadku jest pierwszym wierszem 7-wierszowej tablicy wielowymiarowej). Bez nawiasów kompilator zinterpretowałby to jako int* array[5], czyli tablicę 5 int*.
To dobre miejsce na użycie automatycznego dedukcji typów:
int x { 7 }; // non-constant
auto array { new int[x][5] }; // o wiele prostsze!Niestety to stosunkowo proste rozwiązanie nie działa, jeśli wymiar tablicy znajdujący się najbardziej na prawo nie jest stałą czasową kompilacji. W takim przypadku musimy trochę bardziej skomplikować sprawę. Najpierw przydzielamy tablicę wskaźników (jak powyżej). Następnie iterujemy po tablicy wskaźników i przydzielamy dynamiczną tablicę dla każdego elementu tablicy. Nasza dynamiczna tablica dwuwymiarowa jest dynamiczną jednowymiarową tablicą dynamicznych jednowymiarowych tablic!
int** array { new int*[10] }; // przydziel tablicę 10 wskaźników int — to są nasze wiersze
for (int count { 0 }; count < 10; ++count)
array[count] = new int[5]; // to są nasze kolumnyMożemy wtedy uzyskać dostęp do naszej tablicy w zwykły sposób:
array[9][4] = 3; // To żart samo, co (tablica[9])[4] = 3;Dzięki tej metodzie, ponieważ każda kolumna tablicy jest dynamicznie alokowana niezależnie, możliwe jest utworzenie dynamicznie alokowanych tablic dwuwymiarowych, które nie są prostokątne. Na przykład możemy utworzyć tablicę w kształcie trójkąta:
int** array { new int*[10] }; // przydziel tablicę 10 wskaźników int — to są nasze wiersze
for (int count { 0 }; count < 10; ++count)
array[count] = new int[count+1]; // to są nasze kolumnyW powyższym przykładzie zauważ, że tablica[0] to tablica o długości 1, tablica[1] to tablica o długości 2 itd…
Cofnięcie alokacji dynamicznie alokowanej tablicy dwuwymiarowej przy użyciu tej metody również wymaga pętli:
for (int count { 0 }; count < 10; ++count)
delete[] array[count];
delete[] array; // należy to zrobić na końcuPamiętaj, że usuwamy tablicę w odwrotnej kolejności, w jakiej ją utworzyliśmy (najpierw elementy, potem tablica sam). Jeśli usuniemy tablicę przed kolumnami tablicy, będziemy musieli uzyskać dostęp do zwolnionej pamięci, aby usunąć kolumny tablicy. A to skutkowałoby niezdefiniowanym zachowaniem.
Ponieważ przydzielanie i cofanie alokacji tablic dwuwymiarowych jest złożone i łatwo je zepsuć, często łatwiej jest „spłaszczyć” tablicę dwuwymiarową (o wymiarach x na y) w jednowymiarową tablicę o rozmiarach x * y:
// Zamiast do:
int** array { new int*[10] }; // przydziel tablicę 10 wskaźników int — to są nasze wiersze
for (int count { 0 }; count < 10; ++count)
array[count] = new int[5]; // to są nasze kolumny
// Zrób to
int *array { new int[50] }; // tablica 10x5 spłaszczona w pojedynczą tablicęProstej matematyki można następnie użyć do przekształcenia indeksu wiersza i kolumny prostokątnej tablicy dwuwymiarowej na pojedynczy indeks tablicy jednowymiarowej:
int getSingleIndex(int row, int col, int numberOfColumnsInArray)
{
return (row * numberOfColumnsInArray) + col;
}
// ustaw tablicę[9,4] na 3, używając naszej spłaszczonej tablicy
array[getSingleIndex(9, 4, 5)] = 3;Przekazywanie wskaźnika przez adres
Podobnie jak możemy użyć parametru wskaźnika do zmiany rzeczywistej wartości przekazanego argumentu bazowego, możemy przekazać wskaźnik do wskaźnika do funkcji i użyć tego wskaźnika do zmiany wartości wskaźnika, na który on wskazuje (jeszcze zdezorientowany?).
Jeśli jednak chcemy, aby funkcja mogła modyfikować to, na co wskazuje argument wskaźnika, ogólnie lepiej jest to zrobić, używając zamiast tego odniesienia do wskaźnika. Zostało to omówione w lekcji 12.11 — Przekazywanie adresu (część 2).
Wskaźnik na wskaźnik do wskaźnika na…
Możliwe jest również zadeklarowanie wskaźnika na wskaźnik do wskaźnika:
int*** ptrx3;Można tego użyć do dynamicznego przydzielania tablicy trójwymiarowej. Jednakże wymagałoby to pętli wewnątrz pętli i jest niezwykle skomplikowane w poprawnym wykonaniu.
Możesz nawet zadeklarować wskaźnik do wskaźnika do wskaźnika do wskaźnika:
int**** ptrx4;Lub wyżej, jeśli chcesz.
Jednak w rzeczywistości nie są one zbyt przydatne, ponieważ nieczęsto potrzeba tak wielu poziomów pośrednich.
Wnioski
Zalecamy unikanie używania wskaźników do wskaźników, chyba że nie są dostępne żadne inne opcje, ponieważ są one skomplikowane w użyciu i potencjalnie niebezpieczne. Wyłuskanie wskaźnika zerowego lub wiszącego za pomocą zwykłych wskaźników jest dość łatwe — jest to podwójnie łatwe w przypadku wskaźnika do wskaźnika, ponieważ trzeba wykonać podwójne wyłuskanie, aby dostać się do wartości bazowej!

