W tym materiale zapoznam Cię z absolutnie kluczowym elementem języka C 😲. Element stanowiący kręgosłup samego pojęcia "referencji". Jest nim wskaźnik! Dowiesz się czym w języku C jest wskaźnik, jak go rozpoznać w kodzie, co się pod nim kryje oraz jakie nam oferuje możliwości! Do dzieła 💥!

WSKAŹNIK W JĘZYKU C. NARZĘDZIE OGROMNIE POSZERZAJĄCE MOŻLIWOŚCI

Pojęcie "wskaźnik" i wszystkie zagadnienia związane z nimi, to temat rzeka na co najmniej kilka osobnych artykułów 🤯. Ten materiał traktuj jak wprowadzenie - najlżejszy fragment z całego rozdziału dotyczącego zarządzania pamięcią 🫵! Zaczniemy sobie od teorii samego terminu i ukazaniu prostego przykładu, tak abyśmy mogli delikatnie przesuwać się wgłąb tego obszernego tematu 👍.

DEFINICJA

Wskaźnik jest to typ danych przechowujący odniesienie do innej naszej danej. Odniesienie, czyli inaczej, referencja. Napisałem "danej", gdy to nie musi być zmienna. Wskaźnik może też wskazywać na funkcję i w ten sposób jest możliwe m.in. tworzenie wywołań zwrotnych (ang. callback) i funkcji wyższego rzędu, natomiast nie wchodźmy teraz w takie detale 🙃.

Wskaźnik w języku C jest na samym początku trudny do opanowania 🧠. Wymaga od człowieka "przestawienia się" na inny tok myślenia o uzyskiwaniu pożądanego efektu lub odpowiedniej struktury, zwłaszcza jeśli już programujesz w językach wyższego poziomu, takich jak Java czy C#, w których nie musisz się przejmować manualnym zarządzaniem pamięcią, bo to robi za Ciebie kompilator, a "gotowce" w postaci wyrażeń lambda i innych elementów wykorzystujących wskaźniki, są na wyciągnięcie ręki 🙂.

Język C jest od tej strony bezlitosny i jeżeli masz ochotę wykorzystać pełnię jego możliwości, to musisz bardzo dobrze operować wskaźnikami 🔥! Nie tylko znać znak asterysku i ampersandu, natomiast też doskonale wiedzieć czego użyć w danej sytuacji oraz w jaki sposób skonstruować wyrażenie z wplecionym wskaźnikiem, aby uzyskać strukturą działającą identycznie, jak coś gotowego w językach o zwiększonej abstrakcji (np. wyrażenie lambda, o którym wspomniałem) 💣!

PRAKTYCZNE ZASTOSOWANIA WSKAŹNIKÓW

Wskaźniki dają mnóstwo możliwości, których nie osiągnie się bez ich udziału. Tak naprawdę dopiero one odkrywają cały potencjał języka C 👑. Oto co w dużym skrócie jest możliwe dzięki wskaźnikom 👇:

  1. pobieranie wartości przez operator "wyłuskania" (dereferencji) ze swobodnym "przestawianiem" danego adresu pamięci ➡️ "wydobywanie" wartości przez wskaźnik, który "wskazuje" na daną komórkę pamięci,
  2. przekazywanie wartości typów danych do funkcji przez referencję ➡️ utrwalanie modyfikacji wartości zmiennych typu prostego po zakończeniu funkcji, bez konieczności zwracania wartości,
  3. definiowanie wyrażeń lambda ➡️ "wplatanie" funkcji jako parametru innej funkcji umożliwiające stosowanie "wymiennych instrukcji" co każde wywołanie (element programowania funkcyjnego),
  4. definiowanie funkcji wyższego rzędu ➡️ operowanie na funkcjach przyjmujących wyrażenie lambda jako parametr bądź zwracające wskaźnik do innej funkcji (również element programowania funkcyjnego),
  5. zarządzanie dynamicznymi strukturami danych np. listami powiązanymi ➡️ stosowanie zbiorów danych z elastycznie dopasowującym się rozmiarem podczas działania programu.

Wszystko co wymieniłem, jest możliwe do osiągnięcia wyłącznie z użyciem wskaźników 🚀! Masz co najmniej 5 solidnych argumentów, żeby zainteresować się ich nauką 😄!

Zaznaczę (już na uboczu), że tablica, choć jest statyczną strukturą danych, także ma ścisły związek ze wskaźnikami - ona sama jest wskaźnikiem 😱! Łańcuchy znaków również opierają się o wskaźniki i wszystkie podstawowe funkcje operujące na nich 🔥!

PIERWSZY KONTAKT ZE WSKAŹNIKIEM

Przejdźmy teraz do zaznajamiania się ze wskaźnikiem od strony kodu. Tworzone są za pomocą znaku asterysku, czy jak wolisz, gwiazdki (*) umieszczanej pomiędzy typem danych, a nazwą 👇:

int* pointerToNumber;

Wskaźnik przechowuje adres pamięci i to za jego pośrednictwem powstaje "relacja" z danym obiektem danych (czyt. zmienną bądź funkcją ℹ️). Aczkolwiek, nic nas nie broni przed utworzeniem wskaźnika bez przypisania adresu 🙄. Jak się domyślasz, nie wróży to niczego dobrego ❌.

Pod żadnym pozorem nie operuj na niezainicjowanym wskaźniku ⛔! Wskaźnik w języku C musi mieć poprawnie przypisany adres, aby bezpiecznie zarządzać pamięcią poprzez uzyskiwanie dostępu i modyfikację ⚠️! Zignorowanie tej informacji przyniesie Ci rezultat w postaci niezdefiniowanego zachowania 💥!

DEFINICJA ZMIENNEJ WSKAŹNIKOWEJ

Pierwsza rzecz jaką musisz przyswoić, to to, żeby od razu definiować wskaźnik! Tylko jak to zrobić? Dajmy na to, że mamy liczbę całkowitą 👇:

int number = 5;

i chcemy dodać wskaźnik, aby wskazywał na tę liczbę celem późniejszej modyfikacji wartości przez ten wskaźnik. Wtedy możemy zapisać tak:

int* pointerToNumber = &number;

W tym momencie poznajesz drugi operator związany z referencjami - ampersand (&)! On pobiera adres danej zmiennej 😲!

WYŚWIETLANIE ADRESU PAMIĘCI

Gdybyśmy chcieli wyświetlić uzyskany adres, możemy skorzystać z funkcji "printf" i skorzystać ze specyfikatora "%p" stworzonego do wypisywania adresów w systemie szesnastkowym (heksadecymalnym):

printf("%p\n", pointerToNumber);

Nie zdziw się, jeśli ujrzysz inny adres za każdym razem, gdy uruchamiasz ten sam program 👀. To jest normalny proces zarządzania pamięcią przez system operacyjny 😊.

W języku C istnieją 2 wyjątki niewymagające ampersandu: tablice i łańcuchy znaków, gdyż są to dane same w sobie będące wskaźnikami. Wstawiając ich nazwę, już od razu uzyskujesz adres do pierwszego argumentu, z racji "bycia" typem referencyjnym ℹ️.

UZYSKIWANIE DOSTĘPU DO WARTOŚCI POPRZEZ OPERATOR "WYŁUSKANIA" (DEREFERENCJI)

Jeżeli potrzebujesz uzyskać dostęp do wartości "wskazywanej" przez wskaźnik, używasz tego samego znaku asterysku i dopisujesz go przed nazwą zmiennej:

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

lecz uwaga - tutaj będzie zupełnie inne znaczenie tego operatora 🔴! Gdy wstawiasz asterysk w tym miejscu, to wykonujesz tak zwane "wyłuskanie" wartości (dereferencję), czyli "wydobywasz" wartość ze wskaźnika jaki w danej chwili obowiązuje ℹ️. Aby nie rozkładać wprowadzenia na wiele wątków naraz, dalsze szczegóły znajdziesz w odrębnym artykule.

Zobacz, że chcąc wyświetlić tę wartość przez "printf", wtedy korzystasz z innego specyfikatora zgodnego z typem danych - w tym przypadku "%d", bo wartością "wydobytą" będzie "int" 💡.

LEPIEJ POSTAWIĆ ZNACZEK Z LEWEJ CZY Z PRAWEJ STRONY?

Od razu wyjaśnijmy sobie taki niuansik. Możesz zauważyć, że ktoś pisze tak 👇:

int* pointerToNumber;

a ktoś inny wstawia asterysk przed nazwą zmiennej:

int *pointerToNumber;

Od razu piszę: obie formy są równoważne. To jest wyłącznie kwestia upodobania estetycznego, natomiast dla kompilatora nie ma znaczenia gdzie umieścisz ten znaczek 😄. Ja umieszczam go po typie danych, bo jednak jest to modyfikacja typu, a nie samej zmiennej, natomiast może mylić w momencie zadeklarowania wielu zmiennych w jednym wierszu:

int* pointerToNumber, *pointerToNumberB;

Wtedy może być lepiej zostawienie asterysku "przyklejonego" do nazwy 😉.

PRZYKŁAD KODU ŹRÓDŁOWEGO

Na finał, prosty kod źródłowy, który prezentuje jak zdefiniować wskaźnik, prawidłowo przypisać adres i jak "wydobyć" wartość poprzez dereferencję 👇:

#include <stdio.h>

void printAddressToVariable(int*);
void printValueByAddress(int*);

int main(void)
{
	int number = 5;
	int* pointerToNumber = &number;

	printAddressToVariable(pointerToNumber);
	printValueByAddress(pointerToNumber);

	*pointerToNumber += 20;

	printValueByAddress(pointerToNumber);

	return 0;
}

void printAddressToVariable(int* pointerToNumber)
{
	printf("Adres do zmiennej: %p\n", pointerToNumber);
}

void printValueByAddress(int* pointerToNumber)
{
	printf("Liczba: %d\n", *pointerToNumber);
}

Tłumaczę na szybko całość. Podzieliłem cały kod na prototypy funkcji w ramach utrwalania przez Ciebie dobrej praktyki wydzielania kodu na mniejsze części 🧩. Obie funkcje korzystają z funkcji "printf" i przyjmują jeden parametr typu "wskaźnik do liczby całkowitej" (zobacz, nie do samej liczby całkowitej 👀!).

W środku funkcji "main", definiujemy sobie liczbę 5 i wskaźnik przechowujący adres do tej zmiennej. Następnie wywołujemy funkcje do wypisania aktualnego adresu pamięci, a potem samej wartości "kryjącej się" pod danym adresem.

O wiele bardziej intrygująca może być następująca instrukcja:

*pointerToNumber += 20;

Jednym prostym zdaniem: ">>wyłuskaj<< wartość ze wskaźnika i dodaj do niej 20". Operator dereferencji pozwala na korzystanie z operatorów modyfikujących wartość zmiennej, więc taki zapis jest poprawny ✅. To jest to samo, jakbyś napisał(a) tak:

number += 20;

A ponieważ wskaźnik jest "skierowany" na tę zmienną, to przy "wyłuskaniu" wartości również będzie "nowa" liczba (po modyfikacji) 😲! To są naczynia połączone, więc przy zmianie wartości danej "podpiętej" pod wskaźnik, zmienna RÓWNIEŻ ulega zmianie!

Wskaźnik w języku C

Wskaźnik w języku C to potężna broń do zarządzania pamięcią dynamicznie (w trakcie działania programu), która pozwala na definiowanie struktur o olbrzymich możliwościach!


To wszystko co widziałbym na wprowadzenie do wskaźników - możesz się domyślać ile jeszcze można o tym napisać 😁!

PODOBNE ARTYKUŁY