Tematem w tym artykule jest niejaka notacja wskaźnikowa w języku C - następna nietypowa część składni jakiej nie znajdziesz w językach Java, C#, JavaScript, Python itp. Zagadnienie stanowi kolejną część rozdziału dotyczącego tablic (stąd zalecane jest przeczytanie wprowadzenia 😉). Jak wstęp grzecznie przeczytany, to zapraszam 😄!

NOTACJA WSKAŹNIKOWA W JĘZYKU C JAKO DRUGI SPOSÓB ODWOŁYWANIA SIĘ DO WARTOŚCI ELEMENTÓW W TABLICY

W sumie, napisałem już w nagłówku o co chodzi 😁. Notację tablicową poznaliśmy wcześniej, a teraz przybliżę Ci zapis z użyciem wskaźnika - notację wskaźnikową 💥! Gdy masz tablicę, to jeżeli zapoznałeś(-aś) się z artykułem o jej drugim "wcieleniu", to wiesz, że możemy uzyskiwać dostęp do jej elementów na 2 sposoby 👇:

  1. poprzez indeksy (nawiasy kwadratowe),
  2. poprzez wskaźniki ("poruszanie się" według bajtów tzw. "arytmetyka wskaźników").

Zdecydowana większość obecnie wykorzystywanych języków programowania obsługuje wszystkim znany zapis "dostawania się" do konkretnego argumentu tablicy za pomocą nawiasów kwadratowych (sposób nr 1) 👇:

int number = numbers[i];

Notacja wskaźnikowa w języku C pozwala na przemieszczanie się po elementach w drugi możliwy sposób, łącząc arytmetykę wskaźników z operatorem "wyłuskania" (dereferencji) 2️⃣:

int number = *(numbers + i);

To daje ten sam efekt - pobranie aktualnej wartości i-tego elementu tablicy 🌟. Wyjaśniam poszczególne części wyrażenia.

ANALIZA WYRAŻENIA

Zacznijmy od porównania obu poznanych notacji odwołujących się do pierwszego elementu. W notacji tablicowej, piszemy w następujący sposób 👇:

numbers[0]

Chcąc dostać się do pierwszego elementu poprzez zapis wskaźnikowy, korzystamy z operatora "wyłuskania" (dereferencji), a ponieważ sama nazwa zmiennej tablicowej to zarazem adres pierwszego elementu, to można to sprowadzić do prostej postaci 😎:

*numbers

To jest odniesienie się do pierwszego elementu. Po podstawieniu np. do funkcji "printf":

printf("%d\n", *numbers);

ujrzymy taką samą liczbę, jaka została przypisana w definicji dla pierwszego elementu.

Zawsze możesz przypisać "wyłuskaną" wartość do zmiennej:

int valueOfFirstElement = *numbers;

i przypisać już bezpośrednio tam, gdzie chcesz:

printf("%d\n", valueOfFirstElement);
"PRZEJŚCIE" DO KOLEJNYCH ELEMENTÓW

Idziemy dalej - jak przechodzić do następnych argumentów 🤔?

Stosujemy arytmetykę wskaźników, czyli "przesunięcie się" o określoną liczbę elementów względem początku tablicy. Przypuśćmy, że chcemy uzyskać adres do drugiego elementu. Co to oznacza dla prawej strony równania?

numbers + 1 = &numbers[1]

Czyli "przesunęliśmy" adres o jeden argument do przodu (konkretnie, to o 4 bajty typu "int") i w ten sposób uzyskaliśmy adres drugiego argumentu tablicy pod indeksem 1 (przypominam, że w tablicy, argumenty są liczone od zera!).

Musisz też mieć świadomość, że tak jak w przypadku notacji tablicowej, tutaj również jesteśmy narażeni na "wyjście" poza tablicę i jeżeli "przesuniemy" adres za daleko, to wywołasz niezdefiniowane zachowanie programu 💥!

"WYŁUSKANIE" WARTOŚCI BEZPOŚREDNIO NA PRZESUNIĘTYM ADRESIE

W sytuacji, gdy chcesz pobrać wartość i-tego elementu z tablicy bezpośrednio (bez przypisywania adresu do zmiennej), musisz skorzystać z tego samego wyrażenia w postaci dodania do adresu "przesunięcia" w postaci liczby całkowitej ⏩:

*(numbers + 1)

To spowoduje przedostanie się do wartości drugiego elementu w kolejności 2️⃣.

Zwróć uwagę na jeden szczegół - nawiasy! Jeżeli chcesz "wyłuskać" wartość od razu "na miejscu", to musisz umieścić nawiasy 🛑! Operator dereferencji ma pierwszeństwo przed dodawaniem/odejmowaniem! Napisanie w taki sposób:

*numbers + 1

spowoduje pobranie wartości elementu i zwiększenie jej o jeden ❌! Gdy chcemy pobrać wartość i-tego elementu, to koniecznie otocz dodawanie nawiasami! W ten sposób wymusisz oczekiwaną kolejność wykonywania operacji (najpierw "przesunięcie" adresu, potem pobranie wartości) 👍.

Notacja wskaźnikowa w języku C

Notacja wskaźnikowa w języku C to drugi sposób uzyskiwania dostępu do elementów danej tablicy (obok notacji tablicowej).

PORUSZANIE SIĘ PO TABLICY DWUWYMIAROWEJ

Ten fragment artykułu traktuj jako ciekawostkę, niż konieczną do opanowania naukę 😉. Tutaj pokazuję jak posługiwać się notacją wskaźnikową w przypadku tablic dwuwymiarowych 💣!

Załóżmy, że od tej pory, nasza tablica przyjmuje dwa wymiary, zamiast jednego (jest prostokątna) 👇:

int numbers[2][3];

Pierwsze pytanie: co będzie po drugiej stronie równania przy podaniu samej nazwy?

Pomyśl chwilę, zanim zerkniesz niżej 😏:

numbers = &numbers[0]

Adres do pierwszej tablicy 🤯???

Tablica dwuwymiarowa, to inaczej, tablica tablic. Przechodząc na taki tok myślenia, powinno Ci być lepiej to zrozumieć 🙂. Dla tablicy dwuwymiarowej, początek tablicy wskazuje na pierwszą "podtablicę"! A ta "podtablica" też posiada własne elementy 😲! Przechodząc na jeszcze inne spojrzenie - najpierw przedostajesz się do wiersza tablicy, który sam w sobie składa się z pewnej liczby wartości ℹ️.

"WYŁUSKANIE" WARTOŚCI ELEMENTU TABLICY DWUWYMIAROWEJ

Kolejna rzecz, to jak "wyłuskać" wartość pierwszego argumentu z pierwszej tablicy 🤔. Czy za pomocą samego operatora dereferencji uda się nam to zrobić? Nie 😁:

*numbers = &numbers[0][0]

Pojedynczy operator spowoduje tylko dostanie się do adresu elementu pierwszego wiersza w pierwszej kolumnie. Jeżeli chcemy dostać się do wartości, musimy wykonać "podwójną dereferencję" 😱:

**numbers = numbers[0][0]

Tłumacząc po kolei 😉:

  1. najpierw "stawiamy" nazwę tablicy - otrzymujemy adres wskazujący na pierwszy wiersz,
  2. potem wykonujemy pierwszy raz "wyłuskanie" - otrzymujemy adres wskazujący na element w pierwszym wierszu, w pierwszej kolumnie,
  3. na końcu ponownie wykonujemy "wyłuskanie" - otrzymujemy wartość elementu w pierwszym wierszu, w pierwszej kolumnie.

Dopiero wtedy uzyskujemy dostęp do danych 😅. Z nadzieją, że udało mi się to w miarę prosto wyjaśnić, przejdźmy dalej.

"PRZEMIESZCZANIE SIĘ" PO ELEMENTACH Z UŻYCIEM NOTACJI WSKAŹNIKOWEJ

Zostało nam jeszcze wyjaśnienie poruszania się po tablicy dwuwymiarowej. Uprzedzam, że tutaj naprawdę mogą być duże kłopoty w rozumieniu 💥!

Na początek dajmy na to, że chcemy "przesunąć się" o jedną kolumnę (nie element!). Jak wtedy będzie wyglądać notacja wskaźnikowa? Ano tak 👇:

*numbers + 1 = &numbers[0][1]

Wykonujemy pojedynczą dereferencję na tablicy celem przedostania się do pierwszego wiersza, a następnie zwiększamy wartość adresu o 1 - w ten sposób otrzymujemy adres do pierwszego wiersza, drugiej kolumny.

Teraz zobacz jaki wpływ ma "objęcie" wyrażenia w nawias i dodatkowy składnik na końcu ⚠️:

*(numbers + 1) = &numbers[1][0]
*(numbers + 1) + 1 = &numbers[1][1]

Wynika stąd, że "przesuwanie" adresu po drugim wymiarze jest realizowane bez użycia nawiasów z powodu pierwszeństwa dereferencji - czyli najpierw występuje konwersja na adres pierwszego argumentu:

*(numbers + 1) = &numbers[1][0]

a dopiero potem występuje przesunięcie o jeden argument:

&numbers[1][0] + 1 = &numbers[1][1];

A jak wygląda przedostawanie się do wartości np. pierwszy rząd, pierwsza kolumna 🤔? Trzymaj się mocno:

*(*(numbers + 1) + 1) = numbers[1][1]

Postawię obok siebie wyrażenia odnoszące się do tego samego elementu i przestawię odstępy, żeby było Ci łatwiej dostrzec różnicę 👍:

  *(numbers + 1) + 1  = &numbers[1][1]
*(*(numbers + 1) + 1) =  numbers[1][1]

Zgadnij która z liczb odpowiada za jaki wymiar 😄:

*(*(numbers + 2) + 1) = numbers[2][1]

Z powyższych wzorów wynika, że występuje taki wzór dla i-tego wiersza i j-tej kolumny:

*(*(numbers + i) + j) = numbers[i][j]

Teraz widzisz czemu notacja wskaźnikowa dla tablicy dwuwymiarowej potrafi namieszać w głowie 🤯! Zdaję sobie sprawę, że może to być "małą masakrą" 😅, zwłaszcza gdy widzisz to po raz pierwszy 😊!


Koniec 🏁. Rozumiem, że ten temat może powodować ból głowy, jako że wygląda na niepotrzebne "obchodzenie" wygodnego zapisu z nawiasami kwadratowymi 🙂.

PODOBNE ARTYKUŁY