Skocz do zawartości

[Programowanie] Pułapki języka C


GAndaLF

Pomocna odpowiedź

osmial, jeszcze jest drugie zastosowanie: kiedy korzystasz z komend preprocesora #ifdef i #ifndef. Można wtedy się np. decydować czy dany blok ma być wykonany np. zawsze raz, po sprawdzeniu warunku czy w pętli.

Link do komentarza
Share on other sites

Kiedyś miałem problem właśnie w C, i natknąłem się na sztuczkę z liczbami:

uint32_t total = hours * 3600 + minutes * 60 + seconds;

Problem był taki, że zmienna total po wykonaniu tej linijki była typu uint8_t. Pozostałe zmienne hours, minutes i seconds są uint8_t. Próbowałem też dawać rzutowanie tej całej operacji po znaku '=' na uint32_t ale to nic nie pomagało. Chodzi o to, że trzeba było dopisać do tych liczb UL np. 60UL wtedy typ zmiennej total był taki jaki chciałem uint32_t. No tak i tydzień szukania.

Link do komentarza
Share on other sites

Nie trzeba do wszystkich, wystarczy dopisać to do pierwszej z nich i zreorganizować mnożenie, lub rzucić pierwszą zmienną na odpowiedni typ:

uint32_t total = 3600UL * hours + 60 * minutes+ seconds;
uint32_t total = (uint32_t)hours * 3600 + minutes * 60 + seconds; 

Natomiast należy uważać np. na taki zapis:

uint32_t total = 3600 * hours + 60 * minutes+ seconds;

ponieważ, wbrew pozorom, wynik może być różny w zależności od zastosowanej maszyny. Zadziała tutaj mechanizm zwany promocją do int, tj. kompilator rzuci pierwszą liczbę do inta. I tak dla 8 i 16-bitówców int ma zazwyczaj rozmiar 16-bitów, a dla 32-bitówców zazwyczaj 32-bity.

Link do komentarza
Share on other sites

Właśnie chyba tak próbowałem i nie pomagało mi (pierwszy kod). Dopiero ten trick z dopisaniem UL pomógł, jakby to jakiego typu są liczby dawało pierwszeństwo jeśli chodzi o zwracany typ operacji. Albo być może miałem tak jak napisałeś w drugim kodzie i wtedy tak jak mówisz rzutowało mi do inta

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Generalnie pierwszy element ma znaczenie.

Natomiast chciałbym kolegom przypomnieć o istotnej pułapce, której zdaje się nie zauważać wielu z nas, a która "zadziała" na maszynach 16- i 32-bitowych. Załóżmy, że mam taki kod:

uint8_t buffer[16];
uint16_t z1;
uint32_t z2;

// tutaj kopiujemy coś do bufora, jakieś memcpy itp.

z1 = buffer[0]; // dobrze
z1 = buffer[1]; // źle
z1 = buffer[2]; // teoretycznie dobrze, zależnie od kompilatora i maszyny
z1 = buffer[3]; // źle

z2 = buffer[0]; // dobrze
z2 = buffer[1]; // źle
z2 = buffer[2]; // raczej źle, zależnie od kompilatora i maszyny
z2 = buffer[3]; // źle

Przy przypisywaniu danych z bufora do zmiennych należy uważać na wyrównanie danych w buforze. Jeżeli dane zaczynają się nieparzyście lub nie od wielokrotności 4 (lub 2 dla 16-bitowców), mogą pojawić się problemy. O wiele bezpieczniejszym sposobem jest wtedy wykorzystanie tradycyjnego memcpy:

memcpy(&z1, &buffer[1], sizeof(z1));

Jednak należy tutaj pamiętać o poprawnym endianesie - do takich operacji przydane jest mieć specjalne makra, które potrafią przepisać dany typ zmiennej bez względu na rodzaj maszyny, tj wykryć go w czasie kompilacji, albo mieć przygotowany zestaw dla big i little endian. Przykładowym moim makrem do kopiowania uint16_t z niewyrównanego bufora (który trzyma dane w big endian) jest:

#define _Int16ToVoidBE(buff, val) {_Lo(buff) = _Hi(val); _Hi(buff) = _Lo(val);}

gdzie makra _Lo i _Hi potrafią się odnieść do danych bajtów względem niewyrównanego adresu.

Tyle smaczków na dziś 🙂

Link do komentarza
Share on other sites

Nie do końca zgodzę się z tym co napisał madman07. Zacznijmy od prostego przykładu:

uint8_t x = 5;
uint16_t y = x;

Chyba wszyscy się zgodzą, że niezależnie od architektury, w zmiennej y powinniśmy otrzymać 5, a nie np. 0x1205 ?

Zdarzają się kompilatory, które w ramach optymalizacji, źle kompilują taki kod (nie zerują wyższych bitów), ale to błędy kompilatorów i o nich nie mówmy.

Wracając więc do przykładu:

uint8_t x[2] = { 5, 10 };
uint16_t y = x[0];

Ten kod niczym nie różni się od poprzedniego! Więc poprawna wartość y to nadal 5, a nie 0x0a05.

Więc to co napisał madman07, to moim zdaniem rezultat niepoprawnego działania jakiegoś kompilatora. Faktycznie takie błędy lubią się kompilatorom przytrafiać - wynika to z silnej optymalizacji.

Użycie memcpy jak w przykładzie to w rzeczywistości odpowiednik:

uint32_t z1 = *((uint32_t*)&buffer[1]);

Taki kod nie tylko że jest brzydki, ale może faktycznie źle działać i to nawet na 32-bitowych architekturach. W ARM7TDMI można odwoływać się do pamięci tylko adresami podzielnymi przez 4 - inne odczyty to w rzeczywistości pobranie 4 bajtów i przesunięcie bitowe. Więc taki kod spowodowałby wyjątek dostępu do pamięci.

Link do komentarza
Share on other sites

Mogliśmy się nie zrozumieć 😉 Pierwszy kod jest okej. Natomiast, taki kod (odwołując się do Twojego drugiego przykładu):

uint16_t y = x[0]; // y = 5
uint16_t y = x[1]; // y nie musi być równe 10

Tutaj dochodzimy do konkluzji, czyli tego, jak kompilator rzeczywiście to potraktuje. A potraktuje to tak, jak napisałeś (i co pokazuję poniżej). Użycie memcpy (tj kopiowanie bajt po bajcie) ABSOLUTNIE nie jest odpowiednikiem takiego kodu:

uint32_t z1 = *((uint32_t*)&buffer[1]);

który to kompilator spróbuje przetłumaczyć na instrukcje typu Load-Save zamiast kopiowania bajt po bajcie. W efekcie, dokładnie jak piszesz, mogą stać się niesprecyzowane rzeczy czy nawet wyjątek dostępu do pamięci. To już zależy pewnie od architektury.

Link do komentarza
Share on other sites

Z takim przypadkiem nigdy się nie spotkałem:

uint16_t y = x[1]; // y nie musi być równe 10

Zgodnie ze standardem C kompilator może inaczej rozmieszczać elementy w obrębie struktury i wtedy faktycznie adresy pól będą inne w zależności od wyrównania. Ale tablice, o ile wiem, muszą mieć kolejne elementy w następujących po sobie lokalizacjach.

Praktycznie każdy procesor potrafi odczytywać bajty z dowolnego adresu - trudniej w przypadku słów lub półsłow. Tak jak pisałem - czasem optymalizator "zapomni" wyzerować górne bity, ale to tylko błąd kompilatora.

Czym Twoim zdaniem różni się kopiowanie bajt-po-bajcie od instrukcji Load-Store? O ile wiem ARM nie mają innych instrukcji dostępu do pamięci niż Load-Store... Więc różnica jest tylko taka, że odpowiednie LDR/STR będą wywołane w kodzie memcpy, a nie włączone do programu bezpośrednio.

Jeszcze jedna sprawa - memcpy nie musi kopiować bajt po bajcie. Jej definicja mówi, że kopiuje bloki bajtów, ale optymalizowane implementacje mogą kopiować większe bloki niż jeden bajt na raz. Oczywiście dbając o początkowe i końcowe bajty niezbędne do wyrównania.

Link do komentarza
Share on other sites

Faktycznie, z tablicą się zagalopowałem, przepraszam za wprowadzanie w błąd 😉 Powinienem to zapisać tak, jak jest tutaj poniżej to zapisane.

Natomiast jest różnica pomiędzy

uint32_t z1 = *((uint32_t*)&buffer[1]);

a

memcpy(&z1, &buffer[1], sizeof(z1));

gdy buffer jest tablicą np. typu uint8_t. Jak sam powiedziałeś, memcpy skopiuje dane dbając o wyrównanie (czy implementacja standardowa uwzględnia optymalizację pod postacią kopiowania np. bloków 4 bajtowych?), natomiast rzutowanie i przypisanie tego nie zrobi.

Link do komentarza
Share on other sites

Kod który podałem jest oczywiście anty-przykładem. Tak nie należy robić.

Co do optymalizacji memcpy, to polecam fajny artykuł: http://www.embedded.com/design/configurable-systems/4024961/Optimizing-Memcpy-improves-speed

Natomiast co do implementacji memcpy, to wszystko zależy od wersji. Nie ma czegoś takiego jak standardowa implementacja. Każdy kompilator ma swoją - niektóre optymalizowaną.

Przykładowo:

* stary Apple (optymalizacja dla 16-bitowców) - http://www.opensource.apple.com/source/xnu/xnu-2050.18.24/libsyscall/wrappers/memcpy.c

* Microblaze - https://stuff.mit.edu/afs/sipb/contrib/linux/arch/microblaze/lib/memcpy.c

* Linux - http://lxr.free-electrons.com/source/arch/alpha/lib/memcpy.c

Optymalizacja memcpy to wbrew pozorom ważna sprawa. Kopiowanie "bajt-po-bajcie" potrafi być niesamowicie nieefektywne. Ale to chyba trochę offtop się zrobił 🙂

Link do komentarza
Share on other sites

Cenne spostrzeżenie z tymi typami zmiennoprzecinkowymi. Co do pułapek języka C czy C++ warto używać narzędzi do sprawdzania walidacji sprawdzania jakości kodu w czasie rzeczywistym. Np. do VS jest dużo wtyczek które pomagają wychwycić tego typu błędy czy złe praktyki.

Generalnie, wyznaję zasadę, że najważniejsza w kodzie jest czytelność i nie należy iść na skróty. Dużo robią wcięcia w kodzie, dla wielu osób jest to, mam nadzieję oczywiste.

  • Lubię! 1
Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.