Беззнаковый тип (unsigned type) хранит только неотрицательные числа. Бит,
который у знакового типа уходил на знак, здесь работает на величину: диапазон
unsigned int — от 0 до 4 294 967 295 вместо −2.1·10⁹…2.1·10⁹.
unsigned int count = 0;
unsigned long long big = 0ULL;Переполнение здесь определено
Это главное отличие от знаковых целых типов.
Переполнение знакового целого — Неопределённое поведение. Переполнение
беззнакового — нормальная, описанная в стандарте операция: вычисления в типе
ширины N идут по модулю 2^N, значение «заворачивается». N здесь — ширина
в битах, то есть 32 для обычного unsigned int.
unsigned int x = 0;
std::cout << x - 1 << '\n'; // 4294967295, а не -1Для 32-битного unsigned int 3 000 000 000 + 3 000 000 000 ≡ 1 705 032 704
(mod 2³²), и этот результат одинаков на любой такой системе.
Но «поведение определено» не означает «алгоритм правильный». Если мы считали количество объектов и после прибавления единицы получили ноль, это всё ещё ошибка задачи — просто воспроизводимая.
Отсюда правило: вычитание из беззнакового нуля не даёт отрицательного числа. Оно даёт огромное положительное. Цикл, который отсчитывает назад до нуля, на беззнаковом счётчике не завершается никогда:
for (unsigned int i = 10; i >= 0; --i) { // условие истинно всегда
// ...
}Смешивание со знаковыми
Если в одном выражении встречаются знаковый и беззнаковый типы одинакового ранга, знаковый молча превращается в беззнаковый — и сравнение переворачивается:
int a = -1;
unsigned int b = 1;
if (a < b) { // ложь: -1 превращается в 4294967295
// сюда не попадём
}Компилятор об этом предупреждает, если попросить: -Wall -Wextra выдаёт
comparison of integer expressions of different signedness. Предупреждения
компилятора здесь — не придирки, а ровно тот случай, ради которого они есть.
Правило «при смешивании всегда побеждает беззнаковый» при этом неверно: если знаковый тип имеет больший ранг и вмещает весь диапазон беззнакового, выбирается знаковый. Полная схема — Арифметические преобразования типов.
Ещё одна оговорка: правило про арифметику по модулю относится к типу,
в котором выполняется операция. Малые беззнаковые типы сначала проходят
продвижение, и unsigned short на обычной системе продвигается до знакового
int — арифметики по модулю там уже не будет.
Когда использовать
Соблазнительно объявлять unsigned всё, что «не бывает отрицательным»:
возраст, размер, количество. Google C++ Style Guide
советует так не делать — беззнаковость не защищает от отрицательных значений,
она превращает их в гигантские положительные, то есть меняет явную ошибку
на скрытую.
Беззнаковые уместны, когда:
- нужна битовая арифметика: маски, сдвиги, побитовые операции;
- нужно определённое поведение при переполнении (хеши, циклические счётчики);
- работаете с типами стандартной библиотеки, которые сами беззнаковые
(
size_t,.size()у контейнеров).
Частые ошибки
- Обратный цикл
for (unsigned i = n; i >= 0; --i)— бесконечный. - Сравнение
i < v.size()при знаковомi— предупреждение о signedness, а при отрицательномiещё и неверный результат. - Ожидание, что
unsignedзапретит присвоить отрицательное значение:unsigned int x = -1;компилируется и даёт максимум типа. - Считать, что беззнаковый тип делает безопасным деление на ноль. Не делает: это Неопределённое поведение независимо от знаковости.