Переполнения интегральных типов - одна из самых частых проблем при написании кода, наряду с выходом за границу массива и попыткой записи по нулевому указателю. Поэтому важно знать, как это происходит и какие гарантии при этом нам дает стандарт.
Для беззнаковых типов тут довольно просто. Переполнение переменных этих типов нельзя в полной мере назвать переполнением, потому что для них все операции происходят по модулю 2^N. При "переполнении" беззнакового числа происходит его уменьшение с помощью деления по модулю числа, которое на 1 больше максимально доступного значения данного типа(то есть 2^N, N - количество доступных разрядов). Но это скорее не математическая операция настоящего деления по модулю, а следствие ограниченного размера ячейки памяти. Чтобы было понятно, сразу приведу пример.
Вот у нас есть число UINT32_MAX. Его бинарное представление - 32 единички. Больше просто не влезет. Дальше мы пробуем прибавить к нему единичку. Чистая и незапятнанная плотью компьютеров математика говорит нам, что в результате должно получится число, которое состоит из единички и 32 нулей. Но у нас в распоряжении всего 32 бита. Поэтому верхушка просто отрезается и остаются только нолики.
Захотим мы допустим пятерку, бинарное представление которой это 101, прибавить к UINT32_MAX. Произойдет опять переполнение. В начале мы берем младший разряд 5-ки и складываем его с UINT32_MAX и уже переполненяемся, получаем ноль. Осталось прибавить 100 в двоичном виде к нолю и получим 4. Как и полагается.
И здесь поведение определенное, известное и стандартное. На него можно положиться.
Но вот что со знаковыми числами?
Стандарт говорит, что переполнение знаковых целых чисел - undefined behaviour. Но почему?
Ну как минимум потому что стандарт отдавал на откуп компиляторам выбор представления отрицательных чисел. Как ранее мы обсуждали, выбирать приходится между тремя представлениями: обратный код, дополнительный код и метод "знак-амплитуда".
Так вот во всех трех сценариях результат переполнения будет разный!
Возьмем для примера дополнительный код и 4-х байтное знаковое число. Ноль выглядит, как
000...00, один как 000...01 и тд. Максимальное значение этого типа INT_MAX выглядит так: 0111...11 (2,147,483,647). Но! Когда мы прибавляем к нему единичку, то получаем 100...000, что переворачиваем знаковый бит, число становится отрицательным и равным INT_MIN.Однако для обратного кода те же рассуждения приводят к тому, что результатом вычислений будет отрицательный ноль!
Ситуация здесь на самом деле немного поменялась с приходом С++20, который сказал нам, что у нас теперь единственный стандартный способ представления отрицательных чисел - дополнительный код. Об этих изменениях расскажу в следующем посте.
Don't let the patience cup overflow. Stay cool.
#cpp20 #compiler #cppcore