CategoryProgramowanie

Czy tablica jest wskaźnikiem?

Często podczas nauki C słyszymy, że tablica tak naprawdę jest wskaźnikiem. Oczywiście takie uproszczenie pomaga na początku zrozumieć pewne rzeczy, ale w końcu warto poznać różnice.

Continue reading

Dlaczego w C const nie może być rozmiarem tablicy?

W C++ trwają starania, żeby praktycznie nigdy nie trzeba było korzystać z preprocesora. Jednym z typowych zastosowań define w C jest deklarowanie stałych będących rozmiarami tablic. W C++ od dawna możemy używać w tym celu const, a od jakiegoś czasu (dokładniej od C++11) również constexpr. Jednak kiedy zechcemy przenieść te praktyki do C czeka nas zawód.

Dlaczego nie chcemy używać preprocesora?

Zacznijmy od początku. Po co właściwie mielibyśmy zmieniać praktykę, która istnieje w C od dziesiątek lat? Dlaczego const miałby być lepszy? Otóż preprocesor wykonuje tylko prostą podmianę tekstu i w momencie kompilacji mamy już podmienioną wartość. Przez to z define gorzej radzą sobie sprzętowe debugery (dodatkowe linki na ten temat na końcu artykułu). Poza tym const zawiera informację o typie zmiennej, przez co możemy otrzymać dodatkowe warningi w przypadku podejrzanych operacji. No ale niestety w C nie działa to tak jak w C++.

Tablice globalne

Najczęściej tablice chcemy zadeklarować jako static, aby była zaalokowana na początku programu i widoczna w jednym pliku:

#include <stdint.h>
#include <stdlib.h>

static const size_t MAX_SIZE = 10;
static uint8_t array[MAX_SIZE] = {0};

Jednak podczas kompilacji spotka nas zawód:

<source>:5:16: error: variably modified 'array' at file scope

    5 | static uint8_t array[MAX_SIZE] = {0};

      |                ^~~~~

Błąd kompilacji występuje ponieważ w C const nie oznacza stałej, ale zmienną tylko do odczytu i deklaracja tablicy jest traktowana jak tablica o zmiennym rozmiarze (VLA – variable length array). Tak więc w przypadku tablic globalnych constów sobie nie użyjemy.

A co z tablicami wewnątrz funkcji?

Tutaj mamy dwie możliwości – tablice statyczne i alokowane na stosie. W pierwszym przypadku sytuacja jest identyczna jak z tablicą globalną:

#include <stdint.h>
#include <stdlib.h>

void fun(void)
{
    static const size_t MAX_SIZE = 10;
    static uint8_t array[MAX_SIZE];

    ...
}

I błąd kompilacji:

<source>: In function 'fun':

<source>:7:20: error: storage size of 'array' isn't constant

    7 |     static uint8_t array[MAX_SIZE];

      |                    ^~~~~

Nic dziwnego, w końcu zmienne statyczne wewnątrz funkcji są alokowane tak samo jak globalne, ale są widoczne tylko w jednej funkcji.

Przejdźmy teraz do tablic alokowanych na stosie:

#include <stdint.h>
#include <stdlib.h>

void fun(void)
{
    static const size_t MAX_SIZE = 10;
    uint8_t array[MAX_SIZE];
}

I tu niespodzianka – w końcu kompilacja się powiodła. Ale radość trwa krótko, bo okazuje się, że VLA to rozszerzenie z C99 i jeśli skompilujemy z flagami gcc -ansi -pedantic, znowu otrzymamy błąd:

<source>: In function 'fun':

<source>:7:5: warning: ISO C90 forbids variable length array 'array' [-Wvla]

    7 |     uint8_t array[MAX_SIZE];

      |     ^~~~~~~

Oznacza to, że nie mamy gwarancji, że nasz kod będzie portowalny. Poza tym samo deklarowanie tablic wewnątrz funkcji ma dosyć ograniczone zastosowanie ze względu na ograniczoną pojemność stosu.

Tak więc chyba już wystarczy tych prób – const nie nadaje się jako rozmiar tablicy w C.

Czy mamy jakąś alternatywę?

Jak widać const nie jest w stanie zastąpić define w C. Ale może jest jakaś inna alternatywa? Otóż możemy użyć enuma:

#include <stdint.h>
#include <stdlib.h>

enum
{
    MAX_SIZE = 10
};

static uint8_t array[MAX_SIZE];

Elementy enuma są stałymi liczbowymi, więc mogą być rozmiarami tablic. Dodatkowo są symbolami kompilatora, a nie preprocesora, więc debugery sobie z nimi lepiej poradzą. Natomiast użycie enuma w ten sposób jest dosyć zaskakujące, a zyski z tego niewielkie, więc nie ma sensu porzucać praktyki z define używanej od lat siedemdziesiątych.

Jeszcze parę słów o constach

Ogólnie const w C jest dosyć upośledzony względem C++. Tak jak mówiłem wcześniej const w C oznacza zmienną tylko do odczytu. Natomiast define i enum są traktowane jako stałe w czasie kompilacji. Po szczegóły odsyłam na cppreference:

W C++ constexpr i wartości liczbowe przypisane do const są również traktowane jako stałe w czasie kompilacji i dlatego możemy ich używać jako rozmiar tablicy.

Jest jeszcze jedna ważna właściwość const, która wyklucza używanie go do stałych. Może to wydać się dziwne, bo przecież sama nazwa sugeruje takie użycie. Otóż zmienne z modyfikatorem const są alokowane w pamięci, więc jeżeli w pliku zadeklarujemy długą listę stałych jako const możemy znacznie zwiększyć zużycie pamięci. Tutaj jeszcze wpływ ma poziom optymalizacji i czy deklarujemy zmienną z linkowaniem zewnętrznym (bez static), czy wewnętrznym (ze static). Ale nie zmienia to faktu, że define i enum nie mają tego problemu.

Skoro const nie spełnia swego podstawowego założenia – nasuwa się pytanie:

const w C? A komu to potrzebne?

Czasem chcemy zadeklarować stałą o typie złożonym – np. ciąg znaków, tablicę, strukturę. Robimy tak w przypadku lookup table, różnego rodzaju mapperów, maszyn stanów, configów i innych tego typu rzeczy. Wtedy define nam nie pomoże i musimy użyć const.

const jest również bardzo przydatny przy pracy z wskaźnikami. Możemy dzięki temu określić, że wartość pod wskaźnikiem się nie zmieni, albo adres wskaźnika się nie zmieni (opisywałem to kiedyś w tym artykule). Dzięki temu kompilator może dokonywać dodatkowych optymalizacji, a więcej na ten temat dowiesz się wyszukując hasło “const correctness”.

define i enum a debugowanie

Pisząc ten artykuł kierowałem się własnym doświadczeniem z debugowaniem enum i define. Wiele razy widziałem, że enumy są lepiej interpretowane. Nie powinno być w tym nic dziwnego – w końcu kompilator może zawszeć informacje o enumach w informacjach debugowych. Format DWARF przewiduje też sekcję debug_macro na informacje z preprocesora, ale widocznie nie jest to tak dobrze wspierane albo informacje nie są pełne.

Sam temat informacji debugowych jest ciekawy i znalazłem na ten temat kilka fajnych linków:

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

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.

Continue reading

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.

Continue reading

Bool C99 jest lepszy niż Twój

W tym artykule pokażę pewne niuanse związane z typem bool w C. Wartości tego typu są wynikami operacji logicznych i warunkami w ifach i pętlach. Jednak bool nie jest do końca pełnoprawnym typem, a raczej intem w przebraniu. Sytuację próbowano poprawić w standardzie C99, ale teraz problem jest taki, że w różnych kontekstach bool może zachowywać się inaczej zależnie czy korzystamy z boola C99 czy własnego.

Continue reading

Style Guide i Coding Standard – czy to już jakość?

Kiedy zdamy już sobie sprawę, że sprytny kod i mikrooptymalizacje to nie jest dobra droga, dochodzimy do wniosku, że jakość kodu jednak ma znaczenie. A wyrazem dbania o tą jakość kodu jest wprowadzenie odpowiednich reguł. Temu właśnie służą Style Guide i Coding Standard. W C są one szczególnie ważne, bo język pozwala nam na wiele dziwnych rzeczy. Czasem występują jako dwa oddzielne dokumenty, a czasem jako jeden. Różni się również sposób ich użycia. Czasem zawierają rozsądny zestaw zasad, który faktycznie jest przestrzegany w projekcie. Czasem natomiast są tak restrykcyjne i tak niemożliwe do wyegzekwowania, że nikt nawet nie próbuje. Kiedy indziej natomiast każdy i tak wie lepiej. W dwóch ostatnich przypadkach reguły służą głównie do chwalenia się na zewnątrz, że do naszego projektu DODALIŚMY jakość. No właśnie to tak nie działa. Możemy wprowadzić te wszystkie reguły a i tak pisać w sposób zupełnie niezrozumiały dla innych.

Dlatego dzisiejszym artykule porozmawiamy sobie właśnie o zasadach znajdujących się zwykle w tego typu dokumentach. Po co powstały i jak je egzekwować. Oczywiście temat jest tak rozległy, że omówię tylko mały odsetek najczęściej spotykanych reguł.

Continue reading

Sprytny kod w C – nie rób tego

Kiedyś bardzo popularne było pisanie sprytnego kodu. Żeby jak najwięcej zmieściło się w jednej linijce. Żeby oszczędzić sobie nadmiarowego pisania, bo w końcu wiem, że coś się wydarzy pod spodem. Osoba czytająca ten kod mogła jedynie stwierdzić – ale dobry jest ten, kto to napisał, ja nic nie rozumiem. Język C doskonale się do czegoś takiego nadawał. Możemy (nad)używać niejawnego rzutowania, wyrażeń z efektami ubocznymi, magicznych operacji bitowych, wskaźnikowych i innych.

Społeczności skupione wokół innych języków zrozumiały swój błąd i poszły w zupełnie przeciwnym kierunku. Zauważono, że nie opłaca się zyskiwać jednej linijki po to, żeby potem nie dało się zrozumieć co autor miał na myśli. Jednak z C jest inaczej. Pisanie sprytnego kodu weszło nam w krew i przykłady tego typu przeniknęły do materiałów edukacyjnych, do przykładów z internetu, na uczelnie i są przekazywane nowym pokoleniom programistów. W tym artykule przyjrzymy się jak objawia się nasz spryt i jak nieuchronnie prowadzi do strzelenia sobie w stopę.

Continue reading

Praca zdalna w embedded

Na ostatnim livestreamie opowiadałem o pracy zdalnej w embedded. W związku z aktualną epidemią temat jest na czasie i pewnie wiele osób i całych firm szuka podpowiedzi jak przejść na pracę zdalną. Szczególnie jeśli nigdy wcześniej tego nie próbowaliście, a teraz nagle wszyscy muszą pracować z domów. W tym artykule zebrałem najważniejsze informacje, które również omawiam w wersji wideo:

Continue reading

Tablice wskaźników na funkcje

To już ostatni wpis z serii dotyczącej zastosowań tablic w C. Dzisiejszym tematem będą tablice wskaźników na funkcje. Pozwalają one w jednolity sposób obsługiwać różne zachowania programu, czyli są rodzajem polimorfizmu. Czasem bywają niezwykle przydatne.

Continue reading

Maszyny stanów na tablicach

Po lookup table i wyszukiwaniu elementów pora na kolejne zastosowanie tablic – maszyny stanu. Podobnie jak w poprzednich przypadkach, logikę warunkową zastąpimy wyczytywaniem odpowiednich indeksów z tablicy. W przypadku maszyn stanu możemy dzięki temu nie tylko zwiększyć wydajność, ale również drastycznie poprawić utrzymywalność kodu.

Continue reading

© 2020 ucgosu.pl

Theme by Anders NorénUp ↑