Piszę aplikację c dla STM32F105, używając gcc.
W przeszłości (z prostszych projektów), zawsze zdefiniowane jako zmienne char
, int
, unsigned int
, i tak dalej.
Widzę, że to jest wspólne korzystanie z typów zdefiniowanych w stdint.h, takie jak int8_t
, uint8_t
, uint32_t
, itd. To prawda, że w wielokrotnością API, które używam, a także w bibliotece ARM CMSIS od ST.
Wierzę, że rozumiem, dlaczego powinniśmy to robić; aby umożliwić kompilatorowi lepszą optymalizację przestrzeni pamięci. Spodziewam się, że mogą istnieć dodatkowe powody.
Jednak ze względu na zasady promocji liczb całkowitych c, ciągle napotykam ostrzeżenia o konwersji za każdym razem, gdy próbuję dodać dwie wartości, wykonać operację bitową itp. Ostrzeżenie brzmi mniej więcej tak conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]
. Zagadnienie to jest omawiane tutaj i tutaj .
Nie dzieje się tak przy użyciu zmiennych zadeklarowanych jako int
lub unsigned int
.
Podając kilka przykładów, biorąc pod uwagę:
uint16_t value16;
uint8_t value8;
Musiałbym to zmienić:
value16 <<= 8;
value8 += 2;
do tego:
value16 = (uint16_t)(value16 << 8);
value8 = (uint8_t)(value8 + 2);
To brzydkie, ale mogę to zrobić w razie potrzeby. Oto moje pytania:
Czy istnieje przypadek, w którym konwersja z niepodpisanego na podpisany i z powrotem na niepodpisany spowoduje niepoprawny wynik?
Czy są jakieś inne ważne powody, dla których warto używać typów całkowitych stdint.h?
W oparciu o odpowiedzi, które otrzymuję, wygląda na to, że typy stdint.h są ogólnie preferowane, nawet jeśli c konwertuje uint
do int
iz powrotem. To prowadzi do większego pytania:
- Mogę zapobiec ostrzeżeniom kompilatora, używając rzutowania czcionek (np
value16 = (uint16_t)(value16 << 8);
.). Czy po prostu ukrywam problem? Czy jest lepszy sposób, aby to zrobić?
8u
i2u
.value8 += 2u;
ivalue8 = value8 + 2u;
, ale dostaję te same ostrzeżenia.Odpowiedzi:
Kompilator zgodny ze standardami, gdzie
int
był od 17 do 32 bitów, może legalnie zrobić wszystko, co chce, używając następującego kodu:Implementacja, która tego chciała, mogłaby zgodnie z prawem wygenerować program, który nie zrobiłby nic oprócz wielokrotnego wypisywania ciągu „Fred” na każdym porcie portu za pomocą każdego możliwego protokołu. Prawdopodobieństwo przeniesienia programu do implementacji, która zrobiłaby coś takiego, jest wyjątkowo niskie, ale teoretycznie jest to możliwe. Jeśli chciałbyś napisać powyższy kod, aby zagwarantować, że nie będziesz się angażować w Niezdefiniowane Zachowanie, konieczne będzie napisanie tego ostatniego wyrażenia jako
(uint32_t)x*x
lub1u*x*x
. W kompilatorze, któryint
ma od 17 do 31 bitów, to ostatnie wyrażenie odciąłoby górne bity, ale nie zaangażowałoby się w niezdefiniowane zachowanie.Myślę, że ostrzeżenia gcc prawdopodobnie próbują sugerować, że napisany kod nie jest w pełni w 100% przenośny. Są chwile, kiedy naprawdę należy napisać kod, aby uniknąć zachowań, które byłyby niezdefiniowane w niektórych implementacjach, ale w wielu innych przypadkach należy po prostu stwierdzić, że jest mało prawdopodobne, aby kod został użyty w implementacjach, które powodowałyby zbyt irytujące rzeczy.
Zauważ, że użycie typów takich jak
int
ishort
może wyeliminować niektóre ostrzeżenia i rozwiązać niektóre problemy, ale prawdopodobnie spowodują inne. Interakcje między typami podobnymiuint16_t
a regułami promocji liczb całkowitych C są trudne, ale takie typy są prawdopodobnie lepsze niż jakakolwiek alternatywa.źródło
1) Jeśli po prostu rzutujesz z liczby całkowitej bez znaku na liczbę całkowitą o tej samej długości tam iz powrotem, bez żadnych operacji pomiędzy, otrzymasz ten sam wynik za każdym razem, więc nie ma problemu. Ale różne operacje logiczne i arytmetyczne działają inaczej na operandach podpisanych i niepodpisanych.
2) Głównym powodem do użytku
stdint.h
typów jest to, że rozmiar nieco takich typu A są zdefiniowane i równa na wszystkich platformach, co nie jest prawdą dlaint
,long
etc, a takżechar
ma żadnego standardu signess, to może być podpisane lub podpisane przez domyślna. Ułatwia manipulowanie danymi, znając dokładny rozmiar, bez dodatkowych kontroli i założeń.źródło
int32_t
iuint32_t
są równe na wszystkich platformach, na których są zdefiniowane . Jeśli procesor nie ma dokładnie pasującego typu sprzętu, typy te nie są zdefiniowane. Stąd przewagaint
itp., A być możeint_least32_t
itd.Ponieważ numer 2 Eugene'a jest prawdopodobnie najważniejszym punktem, chciałbym tylko dodać, że jest to wskazówka
Również Jack Ganssle wydaje się być zwolennikiem tej reguły: http://www.ganssle.com/tem/tem265.html
źródło
uint32_t
.Prostym sposobem na wyeliminowanie ostrzeżeń jest uniknięcie użycia opcji -Wconversion w GCC. Myślę, że musisz włączyć tę opcję ręcznie, ale jeśli nie, możesz użyć -Wno-konwersji, aby ją wyłączyć. Możesz włączyć ostrzeżenia dla konwersji precyzyjnych znaków i FP za pomocą innych opcji , jeśli nadal chcesz.
Ostrzeżenia -Wconversion są prawie zawsze fałszywie pozytywne, dlatego prawdopodobnie nawet -Wextra domyślnie go nie włącza. Przepełnienie stosu pytanie ma wiele sugestii dotyczących dobrych zestawów opcji. Z własnego doświadczenia wynika, że jest to dobre miejsce na rozpoczęcie:
Dodaj więcej, jeśli ich potrzebujesz, ale szanse na to, że nie będziesz.
Jeśli musisz zachować -Wconversion, możesz nieco skrócić swój kod, rzutując tylko na operand numeryczny:
Nie jest to jednak łatwe do odczytania bez podświetlania składni.
źródło
w każdym projekcie oprogramowania bardzo ważne jest stosowanie przenośnych definicji typów. (nawet kolejna wersja tego samego kompilatora wymaga tego.) Dobry przykład, kilka lat temu pracowałem nad projektem, w którym obecny kompilator zdefiniował „int” jako 8 bitów. Następna wersja kompilatora zdefiniowała „int” jako 16 bitów. Ponieważ nie użyliśmy żadnych przenośnych definicji „int”, ram (skutecznie) podwoił rozmiar i wiele sekwencji kodu zależnych od 8-bitowej int nie powiodło się. Zastosowanie definicji typu przenośnego pozwoliłoby uniknąć tego problemu (setki roboczogodzin na naprawę).
źródło
int
w odniesieniu do typu 8-bitowego. Nawet jeśli robi to kompilator inny niż C, taki jak CCS, rozsądny kod powinien używać jednegochar
lub typu piszącego na maszynie dla 8 bitów, a typu piszącego na maszynie (nie „długiego”) dla 16 bitów. Z drugiej strony, przenoszenie kodu z czegoś takiego jak CCS do prawdziwego kompilatora może być problematyczne, nawet jeśli używa odpowiednich typów czcionek, ponieważ takie kompilatory są często „niezwykłe” pod innymi względami.Tak. N-bitowa liczba całkowita ze znakiem może reprezentować mniej więcej połowę liczby nieujemnych liczb całkowitych jako n-bitowa liczba całkowita bez znaku, a poleganie na charakterystyce przepełnienia jest niezdefiniowanym zachowaniem, więc wszystko może się zdarzyć. Zdecydowana większość obecnych i przeszłych procesorów używa podwójnych uzupełnień, więc wiele operacji robi to samo na podpisanych i niepodpisanych typach całkowych, ale nawet wtedy nie wszystkie operacje dadzą identyczne bitowo wyniki. Naprawdę prosisz o dodatkowe problemy później, kiedy nie możesz dowiedzieć się, dlaczego twój kod nie działa zgodnie z przeznaczeniem.
Chociaż int i niepodpisane mają zdefiniowane rozmiary implementacji, często są one wybierane „inteligentnie” przez implementację ze względu na rozmiar lub szybkość. Generalnie trzymam się tych, chyba że mam dobry powód, aby zrobić inaczej. Podobnie, rozważając, czy użyć int czy niepodpisany, ogólnie wolę int, chyba że mam dobry powód, aby zrobić inaczej.
W przypadkach, w których naprawdę potrzebuję lepszej kontroli nad rozmiarem lub podpisem typu, zwykle wolę albo użyć zdefiniowanego przez system typedef (size_t, intmax_t itp.), Albo stworzyć własny typedef, który wskazuje funkcję danego typ (prng_int, adc_int itp.).
źródło
Często kod jest używany na ARM thumb i AVR (i x86, powerPC i innych architekturach), a 16 lub 32 bity mogą być bardziej wydajne (oba sposoby: flash i cykle) na STM32 ARM nawet dla zmiennej, która mieści się w 8 bitach ( 8-bit jest bardziej wydajny w AVR) . Jednak jeśli SRAM jest prawie pełny, powrót do 8 bitów w przypadku zmiennych globalnych może być rozsądny (ale nie w przypadku zmiennych lokalnych). Jeśli chodzi o przenośność i konserwację (szczególnie w przypadku 8-bitowych wersji), zaletą (bez wad) jest określenie MINIMALNEGO odpowiedniego rozmiaru zamiast dokładnego rozmiaru i typedef w jednym miejscu .h (zwykle pod ifdef) w celu dostrojenia (prawdopodobnie uint_fast8_t / uint_least8_t) podczas przenoszenia / kompilacji, np .:
Biblioteka GNU trochę pomaga, ale zazwyczaj typedefs ma sens:
// ale uint_fast8_t dla OBA, gdy SRAM nie stanowi problemu.
źródło