ПрограммированиеСистемный программист на C

Объясните особенности приведения типов между числами с разным знаком (signed/unsigned) в C, какие возникают подводные камни, как избежать непредвиденных последствий при арифметике и сравнении signed/unsigned типов, и как это влияет на переносимость программ?

Проходите собеседования с ИИ помощником Hintsage

Ответ

В C автоматическое приведение типов работает по принципу "usual arithmetic conversions". При участии в выражении объявленных чисел с разным знаком (signed/unsigned), происходит преобразование следующих правил:

  • Если один из операндов unsigned, а другой signed — signed автоматически приводится к unsigned.
  • Это может привести к неожиданному переполнению, особенно при сравнении или арифметических операциях.
  • Размер типов также влияет: если unsigned больше по разрядности, signed приводится к unsigned.

Пример опасной арифметики:

int a = -1; // signed unsigned int b = 1; printf("%d ", a < b); // всегда false, т.к. a преобразуется к очень большому unsigned

Результат: -1, будучи приведён к unsigned, становится очень большим положительным числом.

Что важно помнить:

  • Всегда явно приводить типы, если возможна путаница со знаком.
  • Следить за размерами типов (int, long, uint32_t и т.д.), чтобы преобразования происходили предсказуемо.
  • Разделять логику работы с signed и unsigned переменными, особенно в граничных проверках и арифметике.

Вопрос с подвохом

Вопрос: Какой результат вернёт выражение (int)(unsigned)-1?

Ожидаемо неверный ответ: "-1, ведь -1 и преобразуем к int."

Правильный ответ: В выражении (unsigned)-1 сначала происходит приведение -1 к unsigned (на платформе с 32 битами это 0xFFFFFFFF), затем обратно к signed int, что также зависит от реализации, но часто это снова -1 (если используется two's complement). Однако, правильнее сказать: Результат зависит от стандартов представления signed чисел, но в большинстве реализаций получится -1.

Пример:

int x = (int)(unsigned)-1; // x == -1 на большинстве платформ

Примеры реальных ошибок из-за незнания тонкостей темы


История

В обработчике строк использовали функцию сравнения размера: если длина строки может быть отрицательной, то программа сообщала об ошибке. Однако, длина была типа size_t (unsigned), и сравнение кода if(length < 0) всегда выдаёт false, что привело к бесконечному циклу и переполнению памяти.


История

При парсинге протокола сетевые пакеты содержали поля как unsigned, а локальные переменные были signed. Из-за переполнения unsigned при обработке некоторых значений возникли неверные расчёты длины пакета, что вылилось в уязвимость переполнения буфера.


История

Модуль сравнения дат в логах хранил дату как unsigned int, а искал диапазон даты в int. Некоторые граничные значения, вместо ожидаемого срабатывания исключения, приводили к некорректной фильтрации записей и потере важных логов.