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 przeczytajcie resztę, a jeszcze sobie za to podziękujecie.

TABLICE W JĘZYKU C I 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 (przejrzyjcie teraz 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 bądźcie ostrożni! W rzeczywistości, język C traktuje tablice osadzone w funkcji jak wskaźniki. 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 to w rzeczywistości 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, która tym razem jest pętlą "while", gdyż poruszając się po adresach nie dysponujemy żadnym indeksem, 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.

Tablice w języku C same w sobie są adresami więc przy wprowadzaniu jej do funkcji nie potrzeba dorzucania znaku "ampersandu" tuż przed jej nazwą. 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ę "SIZE" pomnożone przez liczbę bajtów typu danych tej tablicy (w tym przypadku "int", czyli 4 bajty przy systemie 32-bitowym - 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 wyciągnięcie 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 "++i" znajdującego się w pętli "for" w pierwszym przykładzie.

W związku z tym widzimy, że tablice w języku C kryją 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, a nie od razu przerzucać się na Rust, jak twierdził taki jeden na Quora.


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 ekspertów, którzy na serio chcą pisać w języku C i C++.

PODOBNE ARTYKUŁY