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 kompilacji. Jak 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 + 1powoduje "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 😇!
![]() |
"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 🥊.
