Inicjalizacja tablic

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ć, że 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ć za pomocą 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 jeżeli 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, że 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ć, że jest to nieoficjalne rozszerzenie języka i nie jest portowalne. Dlatego jeżeli 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 za pomocą fora. Natomiast warto sprawdzić, czy nasz kompilator może wykonać podobną inicjalizację za pomocą rozszerzenia języka nie będącego częścią oficjalnego standardu. Jeżeli nie mamy specjalnych wymagań dotyczących portowalności możemy skorzystać z tej opcji.

Jeżeli 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ą.

Podobał się artykuł? Zapisz się na newsletter

Zapisz się, aby otrzymywać informacje o nowościach na blogu, ciekawe materiały oraz informacje marketingowe. W prezencie otrzymasz również dokument "Jak zwiększyć jakość kodu w projektach embedded? - darmowe narzędzia".

3 Comments

  1. Tomasz Różański

    18 lipca 2020 at 18:43

    Makro `VALUES_SIZE` zadziała tylko dla tablicy o nazwie `values`. Osobiście wolę używać uniwersalnego makra, które przyjmuje nazwę tablicy jako argument:
    “`c
    #define ARRAY_SIZE(a) (sizeof((a))/sizeof((a[0])))
    “`

    • Tomasz Różański

      18 lipca 2020 at 18:45

      Przepraszam za krzaki w pierwszym komentarzu; myślałem, że można formatować kod za pomocą Markdowna.

    • GAndaLF

      18 lipca 2020 at 19:39

      Cześć Tomek!

      Faktycznie można zrobić uniwersalne makro na rozmiar tablicy. Ja akurat zwykle używam macro bez argumentów, bo pisząc VALUES_SIZE nie musisz wiedzieć, czy tablica jest zadeklarowana ze stałą liczbą elementów, czy nie. Można by też zrobić podwójne macro – docelowe bez argumentu używa pod spodem uniwersalnego.

Dodaj komentarz

Your email address will not be published.

*

© 2020 ucgosu.pl

Theme by Anders NorénUp ↑