19,4 — Wskaźniki do wskaźników i dynamicznych tablic wielowymiarowych

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; // pointer to an int, one asterisk

Wskaźnik na wskaźnik na int jest deklarowany za pomocą dwóch gwiazdki

int** ptrptr; // pointer to a pointer to an int, two asterisks

Wskaź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'; // Dereference pointer to int to get int value

int** ptrptr { &ptr };
std::cout << **ptrptr << '\n'; // dereference to get pointer to int, dereference again to get int value

Powyż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 }; // not valid

Dzieje 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] }; // allocate an array of 10 int pointers

Dział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] }; // rightmost dimension must be constexpr

Nawiasy 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] }; // so much simpler!

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] }; // allocate an array of 10 int pointers — these are our rows
for (int count { 0 }; count < 10; ++count)
    array[count] = new int[5]; // these are our columns

Możemy wtedy uzyskać dostęp do naszej tablicy w zwykły sposób:

array[9][4] = 3; // This is the same as (array[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] }; // allocate an array of 10 int pointers — these are our rows
for (int count { 0 }; count < 10; ++count)
    array[count] = new int[count+1]; // these are our columns

W 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; // this needs to be done last

Pamię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:

// Instead of this:
int** array { new int*[10] }; // allocate an array of 10 int pointers — these are our rows
for (int count { 0 }; count < 10; ++count)
    array[count] = new int[5]; // these are our columns

// Do this
int *array { new int[50] }; // a 10x5 array flattened into a single array

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;
}

// set array[9,4] to 3 using our flattened array
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!

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
265 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze