programowanieProgramista systemowy w C

Wyjaśnij szczegóły rzutowania typów pomiędzy liczbami ze znakiem i bez znaku (signed/unsigned) w C, jakie mogą pojawić się pułapki, jak unikać nieprzewidzianych konsekwencji przy arytmetyce i porównywaniu typów signed/unsigned oraz jak wpływa to na przenośność programów?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

W C automatyczne rzutowanie typów działa na zasadzie "zwykłych konwersji arytmetycznych". W przypadku udziału w wyrażeniu liczb o różnym znaku (signed/unsigned) następuje konwersja na podstawie następujących reguł:

  • Jeśli jeden z operandów jest unsigned, a drugi signed — signed automatycznie rzutuje się na unsigned.
  • Może to prowadzić do nieoczekiwanego przepełnienia, szczególnie podczas porównania lub operacji arytmetycznych.
  • Rozmiar typów również ma znaczenie: jeśli unsigned jest większy pod względem bitów, signed jest rzutowane na unsigned.

Przykład niebezpiecznej arytmetyki:

int a = -1; // signed unsigned int b = 1; printf("%d ", a < b); // zawsze false, ponieważ a jest rzutowane na bardzo dużą wartość unsigned

Wynik: -1, po rzutowaniu na unsigned staje się bardzo dużą liczbą dodatnią.

Co ważne, aby pamiętać:

  • Zawsze jawnie rzutuj typy, gdy istnieje możliwość zamieszania ze znakiem.
  • Zwracaj uwagę na rozmiary typów (int, long, uint32_t itp.), aby konwersje zachodziły w sposób przewidywalny.
  • Oddzielaj logikę pracy z signed i unsigned zmiennymi, szczególnie w kontrolach granicznych i arytmetyce.

Pytanie z pułapką

Pytanie: Jaki wynik zwróci wyrażenie (int)(unsigned)-1?

Oczekiwana błędna odpowiedź: "-1, ponieważ -1 jest rzutowane na int."

Prawidłowa odpowiedź: W wyrażeniu (unsigned)-1 najpierw następuje rzutowanie -1 na unsigned (na platformie 32-bitowej to 0xFFFFFFFF), a następnie z powrotem na signed int, co również zależy od implementacji, ale często to znowu będzie -1 (jeśli używany jest dwuuzupełniający). Jednak lepiej powiedzieć: Wynik zależy od standardów reprezentacji liczb signed, ale w większości implementacji otrzymasz -1.

Przykład:

int x = (int)(unsigned)-1; // x == -1 na większości platform

Przykłady rzeczywistych błędów spowodowanych brakiem znajomości szczegółów tematu


Historia

W przetwarzaniu łańcuchów wykorzystano funkcję do porównywania rozmiarów: jeśli długość łańcucha mogła być ujemna, program zgłaszał błąd. Jednak długość była typu size_t (unsigned) i porównanie if(length < 0) zawsze zwraca false, co doprowadziło do nieskończonej pętli i przepełnienia pamięci.


Historia

Podczas parsowania protokołu pakiety sieciowe zawierały pola jako unsigned, podczas gdy lokalne zmienne były signed. Z powodu przepełnienia unsigned podczas przetwarzania niektórych wartości wystąpiły błędne obliczenia długości pakietów, co przyczyniło się do luki związanej z przepełnieniem bufora.


Historia

Moduł porównywania dat w logach przechowywał datę jako unsigned int, a poszukiwał zakresu dat w int. Niektóre wartości graniczne, zamiast oczekiwanego wywołania wyjątku, prowadziły do niepoprawnego filtrowania zapisów i utraty ważnych logów.