Poznam Cię teraz z drugim wcieleniem tablic w języku C (wprowadzenie do nich znajduje się tutaj)! Możesz się bardzo zdziwić, gdyż tak naprawdę to nie są tablice, lecz wskaźniki do poszczególnych argumentów 🤯! Koniecznie zobacz jak wygląda tablica jako wskaźnik w języku C oraz jak przebiega obsługa elementów z użyciem notacji wskaźnikowej 😲!
TABLICA JAKO WSKAŹNIK W JĘZYKU C. PODEJŚCIE CAŁKIEM NIETYPOWE
Zachęcam do zapoznania się z pierwszą częścią tematu tablic w języku C, gdyż dalej przedstawiona treść jest kontynuacją ⚠️.
PARĘ ZDAŃ WSTĘPU
Prawdziwa "twarz" kryjąca się pod całkiem przyjaznym zapisem tablicowym, to wskaźnik (o samym wskaźniku więcej przeczytasz tutaj) ℹ️. Wskaźnik to odrębny typ zmiennej przechowujący adres innej zmiennej w pamięci. W językach C i C++, adresy są "wydobywane" ze zmiennych za pomocą "ampersandu" (&). W przypadku tablic nie trzeba ich stosować i zaraz zobaczysz dlaczego 😄.
DOWÓD NA "BYCIE" WSKAŹNIKIEM
Mając przykładową tablicę 👇:
int numbers[4] = {56, 7, 43, -1};to gdy wstawisz samą nazwę zmiennej:
numbersuzyskujesz dostęp do adresu pierwszego elementu 👍. Może Cię dziwić, że nie dzieje się nic złego, bez korzystania z operatora do uzyskiwania adresów (&). Ponieważ tablica sama w sobie jest wskaźnikiem, to spełniona jest następująca równość:
numbers = &numbers[0]Czyli początek tablicy to zarazem adres do jej pierwszego elementu ℹ️. Dlatego w tym przypadku, nie musimy wstawiać ampersandu 💡. Warto zapamiętać tę informację 🧠!
"PRZESUWANIE SIĘ" Z UŻYCIEM ADRESU
Teraz pytanie jak dostajemy się do kolejnych argumentów. Tutaj korzystamy z "arytmetyki wskaźników" polegającej na "przesuwaniu się" o określoną liczbę elementów od początku tablicy.
Najpierw podstawiamy adres do pierwszego elementu (bądź dowolnego innego):
int* addressToCurrentElement = numbers;Następnie, chcąc się "przesunąć" o następny element w kolejności, wystarczy inkrementować nasz adres:
++addressToCurrentElement;Teraz zmienna "wskazuje" na element drugi 2️⃣.
W arytmetyce wskaźników, cała sztuka polega na dodawaniu (lub odejmowaniu) o daną liczbę całkowitą, która oznacza o ile elementów wykonać przesunięcie, w jedną lub w drugą stronę ℹ️. W rzeczywistości, to "przesunięcie" odbywa się w bajtach, czyli wartości jaką zwraca operator "sizeof" dowolnego z elementów 👇:
sizeof(numbers[0]);Zwracam na to uwagę, abyś był(a) tego świadomy(-a) 🫵! Jeżeli prawidłowo przypiszesz adres, czyli do tablicy, a nie zupełnie innej struktury (bo wtedy jest to niezdefiniowane zachowanie 💥!), to zważywszy, że wszystkie elementy są jednakowego typu, to program będzie zawsze "przemieszczał się" prawidłowo (nie dojdzie do sytuacji, w której adres będzie "wskazywał" połowę jednego elementu i połowę drugiego) 👍.
Nie przejmuj się, jeśli teraz się gubisz 🙂. Na przykładzie kodu źródłowego pokażę Ci, w jaki sposób "chodzić" po komórkach w tablicy z użyciem pętli 🔁.
PRZEKAZUJĘ PRZEZ REFERENCJĘ "Z URZĘDU"
Na zakończenie zwrócę uwagę, że skoro tablica w języku C jest wskaźnikiem, to od razu "otrzymuje" formę przekazywania przez referencję 🔥. To oznacza, że zmiany w jej elementach z poziomu innych funkcji poza "main", zostaną utrwalone ✅. Nie ma znaczenia czy zdefiniujemy parametr w postaci wskaźnika, czy tablicy ℹ️.
PRZYKŁAD KODU ŹRÓDŁOWEGO
Kody źródłowe jakie Ci zaprezentuję, będą korzystały z odrębnych funkcji. To dlatego, aby Ci zademonstrować fakt, że tablica jako wskaźnik w języku C może zostać wstawiona do parametru zarówno jako tablica, jak i wskaźnik, i nie ma to żadnego wpływu na działanie programu!
PARAMETR TABLICOWY
Proszę bardzo, oto pierwszy program z parametrem typu "tablica" 👇:
#include <stdio.h>
#define ARRAY_SIZE 4
void multiplyNumbers(int[], int);
void printNumbers(int[]);
int main(void)
{
int numbers[ARRAY_SIZE] = {56, 7, 43, -1};
multiplyNumbers(numbers, 2);
printNumbers(numbers);
return 0;
}
void multiplyNumbers(int numbers[], int multiplier)
{
for (int i = 0; i < ARRAY_SIZE; ++i)
{
numbers[i] *= multiplier;
}
}
void printNumbers(int numbers[])
{
for (int i = 0; i < ARRAY_SIZE; ++i)
{
printf("Liczba %d = %d\n", i + 1, numbers[i]);
}
}Prześledźmy sobie wszystko na spokojnie 😅.
U samej góry dorzucamy sobie stałą "#define" do określenia w jednym miejscu wartości oznaczającej rozmiar tablicy, który jak już wiemy, pozostaje definitywny przez cały cykl jej życia 🙂. Następne 2 wiersze, to prototypy funkcji do naszego eksperymentu. Zauważ, że w obu przypadkach korzystamy z tablicy jako typu pierwszego parametru. To umożliwia nam korzystanie z notacji tablicowej w kontekście elementów tablicy wewnątrz funkcji ✅.
Dalej mamy definicje funkcji "main" oraz 2 pozostałych utworzonych dla zobrazowania naszego przykładu. Warto wiedzieć, że mając zmienną do przemieszczania się "indeksowo", możesz odwoływać się do poszczególnych elementów nie tylko w ten sposób:
numbers[i]lecz także używając notacji wskaźnikowej, poprzez operator "wyłuskania":
*(numbers + i)Temat notacji wskaźnikowej przedstawiam w odrębnym materiale 📜.
Gdy poddasz program kompilacji, to zauważysz wypisanie wszystkich 4 liczb przemnożonych przez 2, czyli mamy dowód na przekazywanie przez referencję 👍.
PARAMETR WSKAŹNIKOWY
A teraz drugi przykład, który może Ci zawrócić w głowie 😅. Teraz funkcje akceptujące tablicę, lecz z podstawieniem pod wskaźniki 👇:
#include <stdio.h>
#define ARRAY_SIZE 4
void multiplyNumbers(int*, int*, int);
void printNumbers(int*, int*);
int main(void)
{
int numbers[ARRAY_SIZE] = {56, 7, 43, -1};
int* numbersEndAddress = numbers + ARRAY_SIZE;
multiplyNumbers(numbers, numbersEndAddress, 2);
printNumbers(numbers, numbersEndAddress);
return 0;
}
void multiplyNumbers(int* startAddress, int* endAddress, int multiplier)
{
for (; startAddress < endAddress; ++startAddress)
{
*startAddress *= multiplier;
}
}
void printNumbers(int* startAddress, int* endAddress)
{
for (int i = 1; startAddress < endAddress; (++i, ++startAddress))
{
printf("Liczba %d = %d\n", i, *startAddress);
}
}Mamy zupełnie inne listy parametrów 💥! Zwróć uwagę także na ich liczbę - o jeden więcej! Czas na odpowiedź na pytanie: "w jaki sposób, z użyciem wskaźnika, ustalić gdzie znajduje się koniec tablicy?".
Pamiętasz co napisałem na temat "przesuwania się" od początku tablicy 😊? Mając adres początku tablicy, który "kryje się" za nazwą zmiennej tablicowej (bo tablica sama w sobie jest adresem, więc przy wprowadzaniu jej do funkcji nie trzeba dodawać "ampersandu" tuż przed jej nazwą):
numbersmożemy bez problemu wyznaczyć jej koniec dodając do adresu początkowego...liczbę elementów zawartych w tablicy 💡! Chociaż to jest taki skrót myślowy, bo tak naprawdę, to dodajemy wartość stałej pomnożoną przez liczbę bajtów typu danych tej tablicy 😱. W tym przypadku jest to "int", czyli 4 bajty przy systemie 32-bitowym, co daje 16 bajtów odstępu od pierwszego argumentu.
Spójrz na to:
int* numbersEndAddress = numbers + ARRAY_SIZE;To jest adres pamięci do wartości spoza tablicy! Przypominam, że ostatni element będzie zawierał indeks równy (N - 1), czyli liczba elementów zmniejszona o 1. Tu do adresu początkowego dodajemy liczbę elementów, a więc to będzie wyznacznik końca iterowania po danych tablicy ✅.
Z tego powodu, potrzebne są DWA parametry - jeden wyznacza wskaźnik do pierwszego argumentu tablicy, a drugi prowadzi do wskaźnika do pierwszego adresu wykraczającego poza zakres tablicy. Ponieważ w funkcji występuje pętla "for", musimy mieć warunek zakończenia, gdyż poruszając się po adresach, nie dysponujemy żadnym indeksem i trzeba o to zadbać samodzielnie.
Skoro dotarliśmy do pętli, to zobacz kolejną ważną rzecz 👀! Sama postać pętli ma w sobie dużo nietypowych zmian! Przyjrzyj się 😄:
for (; startAddress < endAddress; ++startAddress)
{
*startAddress *= multiplier;
}W pierwszej pętli nie ma w ogóle inicjowania zmiennej iteracyjnej 🔥!!! Tu polegamy na samych adresach podanych jako parametry! Warunek za to jest ten sam - powtarzaj, dopóki adres początkowy nie "zrówna się" z adresem końcowym. A po ludzku: "dopóki adres odpowiedzialny za iterowanie po elementach, nie dojdzie do końca tablicy".
Adres inkrementujemy (czyli "przesuwamy się" o 4 bajty do przodu), co jest równoważne "przesunięciu się" w notacji tablicowej o jeden element do przodu, a w środku pętli mamy "wyłuskanie" wartości z aktualnego adresu pamięci i przemnożenie przez podany w funkcji mnożnik 🧨.
Teraz analizujemy drugą pętlę:
for (int i = 1; startAddress < endAddress; (++i, ++startAddress))
{
printf("Liczba %d = %d\n", i, *startAddress);
}Tym razem zmienna iteracyjna jest na swoim miejscu, aczkolwiek nie ma nic wspólnego z warunkiem zakończenia! Ona jest tam tylko po to, aby w środku funkcji "printf", poza wypisaniem "wyłuskanej" wartości elementu, wstawić numer argumentu w tablicy 🙂. Co każdy przebieg pętli, inkrementujemy OBIE wartości i zobacz, że te inkrementacje zostały objęte nawiasem. To ma na celu wymusić kolejność wykonywania operacji i zapewnić, że obie te części zostaną wykonane, zanim pętla przejdzie do następnej iteracji.
![]() |
Tablica w języku C, to w rzeczywistości wskaźnik przechowujący adres do każdego z elementów.
Koniec 😅. Widzimy gołym okiem, że tablica jako wskaźnik w języku C, kryje dużo czarniejszą strukturę ⬛. Wskaźniki i adresy oraz umiejętne manipulowanie nimi to temat mogący spędzić sen z powiek 💣. Jeżeli chcesz pisać programy w językach C i C++ w stopniu biegłym, to temat zarządzania pamięcią, musisz mieć w małym palcu!
