Inicjalizacja tablic

ucgosu.pl 4 lat temu

Jakiś czas temu widziałem taki kod inicjalizujący tablicę:

int32_t values[SIZE] = {-1};

Celem autora było zainicjalizowanie wszystkich elementów tą samą wartością. Pewnie dla wielu z Was błąd w tej linijce wyda się oczywisty. Ale skoro

int32_t values[SIZE] = {0};

inicjalizuje wszystkie wartości na zero, to można pomyśleć, iż analogiczny zapis inicjalizuje wszystko na -1. Niestety tylko indeks zerowy przyjmie wartość -1, natomiast cała reszta tablicy będzie mieć wartości zerowe.

Dlaczego tak się dzieje? Co zrobić, aby zainicjalizować tablicę na wartości inne niż 0? Odpowiedzi na te pytania znajdziecie poniżej.

Typowa inicjalizacja

Standardowa inicjalizacja tablicy zawiera typ, nazwę, ilość elementów i wartości początkowe każdego elementu.

type_t array_name[ARRAY_SIZE] = {1,2,3,4,5,6,7,8}; int values[8] = {1,2,3,4,5,6,7,8};

Jeżeli brakuje ilości elementów lub części wartości początkowych, kompilator przyjmuje wartości domyślne.

Tablica bez rozmiaru

Jeżeli nie podamy rozmiaru, jest on określany na podstawie podanych elementów:

int values[] = {1,2,3,4,5,6,7,8}; //8 elementów

Ostateczny rozmiar tablicy możemy uzyskać dzięki sizeof:

#define VALUES_SIZE (sizeof(values)/sizeof(values[0]))

Taka konstrukcja jest przydatna, kiedy nie mamy z góry ustalonej liczby elementów i podczas implementacji często je dopisujemy. Typowymi przykładami mogą być tablice używane do obsługi kodów błędów, czy jakiś eventów.

Ten sam mechanizm jest również wykorzystywany w przypadku łańcuchów znaków:

char tab[] = "Jakis tekst";

Rozmiar tablicy jest taki jak ilość znaków w łańcuchu wliczając końcowy znak \0.

Domyślna inicjalizacja

Jeżeli lista wartości początkowych zawiera mniej elementów niż rozmiar tablicy, pozostałe indeksy są inicjalizowane zerami. Czyli o ile napiszemy:

int values[8] = {1,2,3,4,5};

Indeksy od 0 do 4 zostaną zainicjalizowane wartościami z listy, a indeksy 5, 6, 7 przyjmą wartość 0. Mamy więc odpowiedź dlaczego przykład z początku działa dla {0} a dla {-1} już nie.

Domyślna inicjalizacja zerami jest szczególnie przydatna dla tablic będących zmiennymi lokalnymi, które są alokowane na stosie. Jak wiadomo, niezainicjalizowane zmienne lokalne mają niezdefiniowane wartości. Możemy więc takim prostym zapisem nadać indeksom wartości zerowe.

void fun(void) { uint8_t buf[8] = {0}; ... }

No dobra, ale jak przypisać inną wartość?

Inicjalizacja w runtime

Najprostszą opcją jest wykorzystanie fora:

int values[SIZE] = {0}; for (i = 0; i < SIZE; i++) { values[i] = -1; }

Niestety w ten sposób nie nadamy wartości w momencie kompilacji i będziemy musieli zrobić to w procedurze inicjalizacyjnej. Zawsze jest to trochę więcej pisania i jakieś dodatkowe operacje.

W niektórych przypadkach możemy też skorzystać z funkcji memset, która ustawia wszystkie bajty na zadaną wartość, ale musimy pamiętać o jednym ważnym szczególe – memset ustawia każdy bajt, a w naszym przykładzie używamy typu int, który może mieć na przykład 4 bajty. Akurat dla -1 nam się upiecze, ponieważ w kodzie U2 ma wartość 0xFF, ale dla -2 zamiast 0xFFFFFFFE dostalibyśmy już 0xFEFEFEFE.

Rozszerzenia kompilatora

Jeżeli używamy GCC możemy skorzystać ze składni specyficznej dla tego kompilatora:

int values[8] = {[0 ... 7] = -1};

Składnia GCC jest na tyle elastyczna, iż możemy w ten sposób inicjalizować również pomniejsze zakresy np. od 0 do 3 na -1 i od 4 do 7 na 1:

int values[8] = {[0 ... 3] = -1, [4 ... 7] = 1};

Więcej w dokumentacji GCC.

Jeżeli korzystasz z innego kompilatora, możesz sprawdzić czy również udostępnia podobną składnię. Musisz jednak pamiętać, iż jest to nieoficjalne rozszerzenie języka i nie jest portowalne. Dlatego o ile chcesz wspierać wiele kompilatorów, albo musisz trzymać się standardu, nie możesz skorzystać z tej opcji.

Inicjalizacja w C99

Na koniec jeszcze warto powiedzieć o opcji inicjalizowania konkretnych indeksów dodanej w C99. Wygląda to tak:

int values[8] = {[2] = 5, [5] = 6}; //zawartosc: 0, 0, 5, 0, 0, 6, 0, 0

Wybieramy indeksy, które chcemy zainicjalizować na konkretne wartości, a reszta jest inicjalizowana zerami.

Możemy też zrobić coś takiego:

int values[8] = {[2] = 1, 2, 3, 4, 5}; //zawartosc: 0, 0, 1, 2, 3, 4, 6, 0

Czyli wartości po przecinku inicjalizują kolejne indeksy po tym wskazanym, natomiast pozostałe wartości są domyślnie inicjalizowane zerami.

Więcej na cppreference.

Niestety część projektów wymaga kompatybilności z C89, więc czasem ze względu na portowalność lub ograniczenia konkretnego projektu również tej opcji nie będziemy mogli użyć.

Podsumowanie

Tak więc konkluzja jest dosyć smutna, bo najpewniejszym sposobem na inicjalizację tablicy na wartości inne niż zero jest podejście łopatologiczne dzięki fora. Natomiast warto sprawdzić, czy nasz kompilator może wykonać podobną inicjalizację dzięki rozszerzenia języka nie będącego częścią oficjalnego standardu. o ile nie mamy specjalnych wymagań dotyczących portowalności możemy skorzystać z tej opcji.

o ile chcesz dowiedzieć się więcej o tym, jak pisać dobry kod w C – przygotowuję właśnie szkolenie online “C dla zaawansowanych”. Wejdź na https://cdlazaawansowanych.pl/ i zapisz się na mój newsletter. W ten sposób informacje o szkoleniu na pewno Cię nie ominą.

Idź do oryginalnego materiału