Dziwi Was tytuł artykułu? Zdziwię Was jeszcze bardziej do tego stopnia, że mogę zaburzyć Wasz dotychczasowy punkt widzenia na tablice w języku C. Może nie samą ich strukturę, ale sposób przetwarzania przez komputer. Tak naprawdę to nie są tablice, ale wskaźniki do poszczególnych argumentów! Koniecznie zobaczcie jak wygląda tablica jako wskaźnik, a jeszcze sobie za to podziękujecie.
Tweet |
TABLICA JAKO WSKAŹNIK. TO JEST ICH PRAWDZIWE OBLICZE
Prawdziwa "twarz" kryjąca się pod całkiem przyjaznym zapisem tablicowym to wskaźnik. Wskaźnik to odrębny typ zmiennej przechowujący adres innej zmiennej w pamięci (tu znajdziecie szczegóły na ten temat). W językach C i C++, adresy są "wydobywane" ze zmiennych za pomocą "ampersandu" (&). To nie jest idealne miejsce na rozgrzebywanie tak trudnego i rozległego tematu jakim są wskaźniki, więc będę starał się przedstawić je powierzchownie. Standardowo rzucam w Waszą stronę dwa kody źródłowe, jeden korzystający z notacji tablicowej, a drugi ze wskaźnika:
#include <stdio.h>
#define SIZE 4
void printNumbers(int array[SIZE]);
int main(void)
{
int numbers[SIZE] = {56, 7, 43, -1};
printNumbers(numbers);
getchar();
return 0;
}
// funkcja wypisująca tablice w języku C - zapis tablicowy
void printNumbers(int array[SIZE])
{
for (int i = 0; i < SIZE; ++i)
{
printf("numbers[%d] = %d\n", i, array[i]);
}
}
Pętla "for" przechodzi przez wszystkie elementy jeden po drugim tak jak się przyzwyczailiśmy. Za koniec podstawiamy stałą liczbę "SIZE", aby nie wykroczyć poza zakres. Pamiętajcie, że w C i C++ nie ma żadnych zabezpieczeń chroniących przed "wyjściem" poza tablicę, więc trzeba o to samemu zadbać! Jednym ze sposobów jest wprowadzenie stałej jak powyżej. W rzeczywistości, w języku C tablica osadzona w funkcji występuje jako wskaźnik! A to doprowadza nas do wniosku, że jest to przekazywanie przez referencję! Poniższy kod źródłowy prezentuje to samo tylko w nieco bardziej przerażającej formie:
#include <stdio.h>
#define SIZE 4
void printNumbers(int *array, int *end);
int main(void)
{
int numbers[SIZE] = {56, 7, 43, -1};
printNumbers(numbers, numbers + SIZE);
getchar();
return 0;
}
// funkcja wypisująca tablice w języku C - zapis wskaźnikowy
void printNumbers(int *array, int *end)
{
int i = 0;
while (array < end)
{
printf("numbers[%d] = %d\n", i, *array++);
++i;
}
}
Jak widzimy, tablice w języku C występuje w rzeczywistości jako wskaźnik do odpowiedniego adresu zmiennej. Patrząc na te wpisy możecie nie uwierzyć, dlatego radzę sobie to skompilować, zarówno jedno jak i drugie. Zwracam uwagę, iż tutaj występują DWA parametry, jeden z nich robi za wskaźnik do pierwszego argumentu tablicy, a drugi prowadzi do wskaźnika do pierwszego adresu wykraczającego poza zakres tablicy. Ten drugi jest po to, aby ustalić warunek zakończenia pętli "while", gdyż poruszając się po adresach nie dysponujemy żadnym indeksem i trzeba go dorobić samodzielnie tak jak jest na tym przykładzie. Zauważcie jednak, że zmienna iteracyjna nie ma nic wspólnego z warunkiem zakończenia! Dorzuciłem go tylko po to, aby zachować ten sam układ komunikatu wyświetlający wartość argumentu wraz z indeksem tablicy, ale on sam nie jest do niczego potrzebny!
Skoro w języku C tablica występuje jako wskaźnik, to musi sama w sobie być adresem, więc przy wprowadzaniu jej do funkcji nie potrzeba dorzucania znaku "ampersandu" tuż przed jej nazwą (szczegóły są tutaj). Dodatkowo, patrzcie w jaki sposób ustala się wartość drugiego parametru czyli koniec zakresu. Początkowo można twierdzić, że bezsensowne jest dodawanie stałej liczby do tablicy. Natomiast jak już napisałem, to jest ADRES! A do tego adresu dodawany jest "offset" czyli ile argumentów zostało przydzielonych do tego obszaru pamięci. W rzeczywistości sprawa komplikuje się jeszcze bardziej bo jeśli chcemy być tacy "naukowi", to tak naprawdę do adresu stanowiącego "początek" tablicy (czyli argument pierwszy pisany jako [0]) dodaje się wartość stałej "SIZE" 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 4*4 = 16 bajtów odstępu od pierwszego argumentu.
Ostatnia rzecz to "wyciąganie" wartości z danego adresu. W funkcji "printf" znajduje się tak zwany operator dereferencji czy też "wyłuskania" (więcej tutaj). Umożliwia on dostanie się do adresu i zwrócenie konkretnej wartości danego typu zgodnego z tablicą. Dlatego też adres pierwszego argumentu to jest nazwa tablicy ("numbers"), a wyrażenie:
*numbers
oznacza wydobycie wartości przechowywanej pod danym adresem. Postinkrementacja powoduje "spóźnione" zwiększenie o jeden, ale czego? Konkretnie to "przesunięcia" adresu o rozmiar typu w bajtach ("int" ma 4 bajty, zatem "przesuwa" adres o 4 bajty). Dlatego też powyższe wyrażenie jest równoważne do zapisu "++i" znajdującego się w pętli "for" w pierwszym przykładzie.
Widzimy gołym okiem, że tablica jako wskaźnik kryje dużo czarniejszą strukturę. Wskaźniki i adresy oraz umiejętne manipulowanie nimi to rzeczywiście jest temat "tylko dla orłów". Dlatego zawsze będę uważał, że każdy prawdziwy programista musi rozumieć ten temat w stopniu biegłym.
To wszystko w tym temacie. Sam całkiem niedawno go zrozumiałem i zgodzę się z Wami, że od prób zrozumienia można dostać mocnego bólu głowy. To już jest wątek tylko dla ludzi, którzy na serio chcą pisać w językach C i C++ na poziomie "proficient".