Inicjalizacja struktur

Ostatnio było o inicjalizacji tablic, dzisiaj pora na struktury. Ostatnio zdarzył mi się projekt, gdzie kompilator nie wspierał inicjalizacji z podawaniem nazwy pól. Dlatego pomyślałem, że dobrze będzie zebrać w jednym miejscu opcje inicjalizacji, od jakich standardów są dostępne i co się dzieje, kiedy nie podamy wartości dla wszystkich pól.

Typowa inicjalizacja

Klasyczna inicjalizacja wszystkich pól struktury bazuje na kolejności elementów i jest trochę podobna do inicjalizacji tablicy:

struct point
{
    float x;
    float y;
    float z;
};

struct point my_point = {0.1f, 0.2f, 0.3f};
//x = 0.1f, y = 0.2f, z = 0.3f

Takie rozwiązanie ma jedną oczywistą wadę – musimy znać kolejność pól struktury, żeby wiedzieć jaka wartość jest przypisana do jakiego parametru. Co więcej, jeśli zechcemy zmienić kolejność parametrów, dodać nowy parametr na początku, czy gdzieś w środku, musimy uwzględnić to we wszystkich inicjalizacjach. Brzmi jak doskonały sposób na wprowadzenie do naszego programu dziwnych błędów.

Niepełna inicjalizacja

Podobnie jak w przypadku tablic – nie musimy jawnie inicjalizować wszystkich pól. Jeżeli lista jest krótsza, pozostałe pola są inicjalizowane zerami:

struct point pt1 = {0.1f, 0.2f}; //z = 0.0f
struct point pt2 = {0.1f}; //y = 0.0f, z = 0.0f

Korzystając z tej właściwości możemy wykorzystać skrócony zapis inicjalizujący wszystkie pola zerami:

struct point pt = {0};

Inicjalizacja konkretnych pól

Standard C99 umożliwia nam podanie nazw pól podczas inicjalizacji:

struct point pt1 = {.x = 0.1f, .y = 0.2f, .z = 0.3f};

Skoro nazwy pól jasno mówią do jakiego pola przypisujemy, kolejność nie ma znaczenia:

struct point pt2 = {.z = 0.1f, .y = 0.2f, .x = 0.3f};

Niepełna inicjalizacja w dalszym ciągu działa, ale dzięki wskazaniu konkretnego pola nie jesteśmy ograniczeni jedynie do ustawiania pierwszego elementu:

struct point pt3 = {.z = 0.1f}; //x = 0.0f, y = 0.0f

Możemy również mieszać oba sposoby inicjalizacji:

struct point pt4 = {.y = 0.1f, 0.2f}; //x = 0.0f, y = 0.1f, z = 0.2f

Jednak moim zdaniem taki miks psuje czytelność i niweluje wszystkie plusy pisania nazw pól. Dlatego nie polecam tak robić.

Unie

Unie mają bardzo podobną składnię do struktur, więc również je tutaj omówię. Jak wiadomo unia różni się od struktury tym, że poszczególne pola zajmują ten sam obszar w pamięci i się nawzajem nadpisują. Dlatego nie ma sensu inicjalizowanie kilku pól unii po przecinku.

Załóżmy, że mamy do czynienia z taką unią:

union u32tofloat
{
    uint32_t u32;
    float f;
};

Klasyczna inicjalizacja pozwala jedynie na inicjalizowanie pierwszego pola:

union u32tofloat my_var = {5};

Dopiero w C99 możemy zainicjalizować taką unię używając floata korzystając ze składni z nazwą pola:

union u32tofloat my_var = {.f = 0.1f};

Skoro jesteśmy przy inicjalizacji unii – pora na zagadkę:

union u
{
    uint8_t u8;
    uint32_t u32;
};
union u my_var = {5};
printf("%d", my_var.u32);

Pierwsze pole unii ma 1 bajt, który inicjalizujemy jakąś wartością. Natomiast drugie pole jest większe. Na jakie wartości zostaną zainicjalizowane pozostałe bajty tej unii? Jaką wartość będzie miało pole u32? Czy zachowanie jest gwarantowane?

Której opcji używać?

Jak wiadomo kwestie związane z czytelnością są sytuacyjne i subiektywne. Ale w większości przypadków opcja z jawnym używaniem nazw pól jest dla mnie zdecydowanie lepsza. Szczególnie jeżeli mamy większe struktury z różnymi typami pól. Dzięki temu zmniejszamy ryzyko głupiego błędu z pomyleniem kolejności elementów i możemy bezpieczniej wprowadzać zmiany. Oczywiście pod warunkiem, że nasz kompilator wspiera taką składnię i że możemy używać C99.

W embedded zdarzają się projekty, gdzie standard C89 jest wymuszony, albo kompilator twierdzi, że wspiera C99 ale jednak okazuje się, że nie do końca. W takiej sytuacji niestety zostaje nam klasyczna wersja.

Jak pomóc sobie warningami?

Jeżeli chcemy otrzymać od kompilatora informację, jeżeli korzystamy z klasycznej inicjalizacji i podaliśmy za mało wartości, naszym przyjacielem będzie flaga GCC -Wmissing-field-initializers. Jest ona również aktywowana razem z -Wextra. Flaga ta jest na tyle mądra, że nie traktuje {0} jako niepełnej inicjalizacji.

Więcej w dokumentacji GCC.

Dla innych kompilatorów warto oczywiście poszukać podobnej flagi.

Dlaczego inicjalizacja jest ważna?

Jeżeli chcemy zainicjalizować zmienną lokalną, albo ustawić jakieś niezerowe wartości na pewno warto znać tę składnię. Zawsze lepiej znaleźć początkowe wartości w miejscu deklaracji niż szukać funkcji inicjalizujących.

Może się wydawać, że ważnym argumentem będzie tutaj również wydajność. Jednak nie wydaje mi się, żeby to był duży zysk. W końcu zmienne globalne na początkowe wartości i tak są ustawiane w runtime przed wejściem do funkcji main. Oczywiście to tylko gdybanie, ostatecznym dowodem w sprawie wydajności są zawsze pomiary. W każdym razie jeżeli i tak chcemy jawnie inicjalizować dla czytelności to każdy ewentualny zysk wydajności również będzie na plus.

Jeżeli chcielibyśmy zadeklarować naszą zmienną jako const, musimy nadać jej wartość podczas inicjalizacji. Później możemy ją już tylko odczytywać i nie mamy możliwości napisania funkcji inicjalizacyjnej.

Innym ciekawym powodem dlaczego warto umieć inicjalizować złożone typy danych jest tentative definition. Na ten temat polecam świetny artykuł Huberta Melcherta. Konkluzja jest taka, że dopóki nie ma przypisania wartości początkowej nasza zmienna jest jedynie deklarowana. W kodzie może być wiele deklaracji tej samej zmiennej, natomiast definicja jest tylko jedna. Jeżeli nie przypiszemy wartości początkowej, nasza zmienna jest jedynie zadeklarowana i może już istnieć zmienna o tej samej nazwie. Po dokładniejszą analizę, przykłady i output z gcc zapraszam na blog Huberta.

Szczerze mówiąc ja zawsze zostawiałem same deklaracje zmiennych globalnych i liczyłem, że kompilator je zainicjalizuje zerami za mnie. I z tego co widziałem większość programistów C również tak robi. Ale jak widać, czasem można się na tym przejechać, a proste = {0}, nie kosztuje nas wiele wysiłku.

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ą.

1 Comment

  1. Lokalne zmienne/struktury można też wyzerować stosując modyfikator 'static’.

    Co do zagadki (nie testowałem, mówię z doświadczenia):
    Ponieważ obydwa pola unii są zmiennymi bez znaku, to starsze bajty 'u32′ zostaną wypełnione zerami. Gdyby oba pola były zmiennymi ze znakiem (int8_t, int32_t), to większe z nich będzie 'sign-extended’, czyli:
    – wypełnione zerami jeśli mniejsze pole >= 0;
    – wypełnione przez 0xFF jeśli mniejsze pole < 0

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *