Dzisiejszy temat to tablica o zmiennym rozmiarze (ang. variable length array), czyli w skrócie "VLA" w języku C. To kolejna możliwość wprowadzona w standardzie C99. Patrząc na termin można snuć niemałe przypuszczenia czym konkretnie jest "zmienny rozmiar". Czyżby język C oferował jakąś prototypową rewolucję w postaci tablicy dynamicznej 🤔? A może chodzi o możliwość wprowadzania do funkcji tablic o różnych rozmiarach, nie tylko sztywno ustawionych w kodzie 💡? Wejdź do środka, to się dowiesz 😊.

CO "VLA" W JĘZYKU C DAJE NAM W PRAKTYCE?

Tablica o zmiennym rozmiarze to zdefiniowanie tablicy z wprowadzonym rozmiarem pochodzącym od zmiennej (zmiennym w czasie działania programu) 👇:

int n = 5;
int numbers[n];

i to jest wszystko 🙃! Początkowo język C nie dopuszczał możliwości tworzenia tablic podając rozmiar inny, niż stała wartość osadzona bezpośrednio w środku nawiasów 🚫.

Także to nie jest żadna tablica dynamiczna, że sobie "żonglujemy" rozmiarami jak chcemy 😁 - taki "hokus-pokus", to tylko przy manualnym zarządzaniu pamięcią 🔮. Pojęcie "zmienny rozmiar" oznacza jedynie możliwość wstawienia zmiennej do określenia rozmiaru 📖.

"VLA" jest szczególnie przydatne przy stosowaniu tablic wielowymiarowych, ponieważ zapis tradycyjny nakazuje umieszczenie stałej długości każdego z wymiarów, z wyjątkiem pierwszego, najbardziej "wysuniętego" na lewo (dalej tłumaczę dlaczego), więc przykłady będą opierały się wyłącznie o tablice dwuwymiarowe ℹ️.

WADA I ZALETA

Co może budzić na początku zdziwienie, to następująca wada ❌. Otóż przy takim zapisie, nie możemy zadeklarować tablicy i za jednym zamachem wprowadzić wartości 👇:

int numbers[ROWS][COLUMNS] = {{...}, {...}, ...};

To jest niedopuszczalne! Natomiast przy definicji "tradycyjnej", już jest dozwolone:

int numbers[2][4] = {{5, 7, 34, -100}, {-97, 56, 8, 42}};

I już tłumaczę dlaczego 📝. Definiowanie wartości wymaga znajomości rozmiaru w czasie kompilacjiJak chcesz określić ile ma być elementów, podczas gdy rozmiar może być nieregularny (zmienny) 😊? Teraz jasne 😉?

A teraz zaleta ✅. "VLA" w języku C jest szczególnie użyteczne przy definiowaniu funkcji dokonujących operacji na tablicach wielowymiarowych ℹ️. Tradycyjnie, musielibyśmy definiować każdą osobną funkcję dla sztywnego rozmiaru drugiego wymiaru w taki sposób:

void printNumbers(int numberOfRows, int array[][NUMBER_OF_COLUMNS])
{
	for (int rowIndex = 0; rowIndex < numberOfRows; ++rowIndex)
	{
		for (int columnIndex = 0; columnIndex < NUMBER_OF_COLUMNS; ++columnIndex)
		{
			printf("%d ", array[rowIndex][columnIndex]);
		}
	}

	printf("\n");
}

Gdyby zaszła potrzeba zastosowania funkcji dla liczby kolumn innej niż stała, to już odpada i trzeba tworzyć nową. A to generuje niepotrzebną duplikację i bałagan ⛔.

A teraz dla porównania, "VLA" w języku C:

void printNumbers(int numberOfRows, int numberOfColumns, int array[numberOfRows][numberOfColumns])
{
	for (int rowIndex = 0; rowIndex < numberOfRows; ++rowIndex)
	{
		for (int columnIndex = 0; columnIndex < numberOfColumns; ++columnIndex)
		{
			printf("%d ", array[rowIndex][columnIndex]);
		}
	}

	printf("\n");
}

Tu mamy możliwość wstawienia tablic o dowolnych wymiarach, zarówno w liczbie wierszy, jak i w liczbie kolumn 👍. Jedna funkcja do każdej tablicy ✅!

Trzeba jeszcze wyjaśnić możliwość zostawienia "pustki" w pierwszym wymiarze (poprzedni fragment kodu) 🔔!

POŁĄCZENIA "WSKAŹNIKOWE"

W języku C, rozmiary wymiarów tablic muszą być jasno określone w nagłówku funkcji. A to dlatego, że tablica w rzeczywistości jest wskaźnikiem i żeby móc przemieszczać się po argumentach "wskaźnikowo", kompilator musi wiedzieć jakiego rozmiaru są wszystkie wymiary.

Gdy odwołujesz się do elementów w taki sposób 👇:

numbers[rowIndex][columnIndex]

to ta instrukcja "pod spodem", zamieniana jest na notację wskaźnikową, która przyjmuje taką postać:

*(*(numbers + rowIndex) + columnIndex)

Wyjątkiem co do wymagalności wprowadzenia rozmiaru, jest pierwszy wymiar najbardziej "wysunięty na lewo" po nazwie. Tablica dwuwymiarowa (NxM) to w rzeczywistości wskaźnik wskazujący na tablicę M-elementową. Inkrementowanie samego adresu takiej tablicy:

numbers + 1

powoduje "przesunięcie się" o jeden wymiar do przodu (w tym przypadku o jeden cały rząd), stąd kompilator musi wiedzieć o ile elementów się "przesunąć", aby ominąć wszystkie elementy należące do pojedynczego rzędu 🔥.

PRZYKŁAD KODU ŹRÓDŁOWEGO

Mając pięknie wyłożone "know-how" na temat "VLA" w języku C 😊, prezentuję Tobie na koniec kod źródłowy ukazujący działanie 👇:

#include <stdio.h>

void initNumbers(int numberOfRows, int numberOfColumns, int array[numberOfRows][numberOfColumns]);
void printNumbers(int numberOfRows, int numberOfColumns, int array[numberOfRows][numberOfColumns]);
void multiplyNumbers(int numberOfRows, int numberOfColumns, int array[numberOfRows][numberOfColumns], int multiplier);

int main(void)
{
	int numberOfRows = 3;
	int numberOfColumns = 3;
	int numbers[numberOfRows][numberOfColumns];

	initNumbers(numberOfRows, numberOfColumns, numbers);
	printNumbers(numberOfRows, numberOfColumns, numbers);
	multiplyNumbers(numberOfRows, numberOfColumns, numbers, 2);
	printNumbers(numberOfRows, numberOfColumns, numbers);
	
	return 0;
}

void initNumbers(int numberOfRows, int numberOfColumns, int array[numberOfRows][numberOfColumns])
{
	for (int rowIndex = 0; rowIndex < numberOfRows; ++rowIndex)
	{
		for (int columnIndex = 0; columnIndex < numberOfColumns; ++columnIndex)
		{
			array[rowIndex][columnIndex] = rowIndex*numberOfColumns + columnIndex + 1;
		}
	}
}

void printNumbers(int numberOfRows, int numberOfColumns, int array[numberOfRows][numberOfColumns])
{
	for (int rowIndex = 0; rowIndex < numberOfRows; ++rowIndex)
	{
		for (int columnIndex = 0; columnIndex < numberOfColumns; ++columnIndex)
		{
			printf("%d ", array[rowIndex][columnIndex]);
		}
	}

	printf("\n");
}

void multiplyNumbers(int numberOfRows, int numberOfColumns, int array[numberOfRows][numberOfColumns], int multiplier)
{
	for (int rowIndex = 0; rowIndex < numberOfRows; ++rowIndex)
	{
		for (int columnIndex = 0; columnIndex < numberOfColumns; ++columnIndex)
		{
			array[rowIndex][columnIndex] *= multiplier;
		}
	}
}

 To już nieco większy kod 😅. U góry mamy zdefiniowane prototypy funkcji i tutaj warto się zatrzymać na małą rzecz ⚠️!

W sytuacji, w której definiujemy parametr typu tablicowego, a rozmiary poszczególnych wymiarów mają zależeć od zmiennych:

int array[numberOfRows][numberOfColumns]

to musimy wstawić nazwę parametru czy tego chcemy, czy nie, ponieważ w języku C (wyjątkowo!) nawiasy kwadratowe umieszczamy po nazwie zmiennej! Jak mi nie wierzysz, to zajrzyj do innych języków gdzie wstawiamy nawiasy kwadratowe (podpowiem, że zazwyczaj po nazwie typu danych 😉) 😄.

W funkcji "main" mamy naszą tablicę "VLA", która "dopasowuje się" do podanych wartości zmiennych ✅. Możesz dać tyle, ile chcesz - na tym polega zaleta tablic o zmiennym rozmiarze 🏆! Natomiast, żeby przypisać jakieś sensowne wartości, należało zastosować obejście 🔁. "VLA" w języku C z uwagi na "zmienne" rozmiary, uniemożliwia przypisanie od razu jakichś wartości w momencie deklaracji . Z tego względu, wpierw wywołujemy funkcję przypisującą wartości poszczególnym elementom, a potem dopiero wykonujemy na nich dalsze operacje 💪.

Podane funkcje również wspierają tablice dwuwymiarowe dowolnych rozmiarów w obu wymiarach! Zauważ, że mamy wprowadzanie liczby rzędów i liczby kolumn, a na końcu jest tablica, której wymiary zależą od dwóch poprzednich wartości ℹ️. Gdy skompilujemy sobie powyższy program, to wszystko wykona się bez zarzutu ✅! A jeżeli będziesz chciał(a) utworzyć sobie nową tablicę 2D, to też śmiało możesz wywoływać te funkcje i nic złego się nie stanie 😇!

Tablica o zmiennym rozmiarze w języku C (VLA)

"VLA" w języku C (ang. variable length array), to możliwość definiowania tablic o zmiennym rozmiarze, która pozwala uniknąć redefiniowania tych samych funkcji dla każdego innego rozmiaru N-tego wymiaru.


Temat zakończony ✔️. Nie odpowiem Ci na pytanie czy opłaca się z tego korzystać, czy nie. To będzie zależało od sytuacji, z jaką przyjdzie Ci się zmierzyć podczas implementowania czegoś w programie 🥊.

PODOBNE ARTYKUŁY