
3.6. Переполнения при операциях с целыми
Рассматривая поразрядные операции, мы ограничились операндами беззнакового типа byte, так как использование знаковых типов требует знакомства с правилами кодирования отрицательных целых чисел. Переменные и константы типа byte могут иметь значения от 0 до 255. Соответствующие двоичные коды - 00000000 (все нули) и 11111111 (все единицы). В то же время для знакового типа sbyte установлены пределы от
-128 до +127. Это связано с принятым на аппаратном уровне правилом кодирования знаковых целых чисел. Для их внутреннего представления используется так называемый дополнительный код. Если к - количество разрядов, отведенное для представления числа х (для sbyte k равно 8), то дополнительный код определяет выражение: |х|, если х >=0, доп (х) = 2к - |х|, если х<0
В битовом представлении чисел с использованием дополнительного кода у всех положительных чисел самый левый бит равен 0, а у отрицательных -единице.
Минимальное число типа sbyte равно -128. Его двоичный код 10000000. Число -1 представлено кодом 11111111. Представление нуля 00000000, код единицы 00000001.
Зная правила двоичного кодирования отрицательных целых чисел, легко понять, как меняется значение переменной знакового типа при поразрядных операциях. Например, применяя к положительному числу операцию поразрядного инвертирования ~, мы меняем знак числа и на 1 увеличиваем его абсолютное значение. При поразрядном инвертировании отрицательного числа результат равен уменьшенному на 1 его абсолютному значению. Следующая программа иллюстрирует применение операции:
//03_05.cs – поразрядное инвертирование знаковых чисел!!!
static void Main05()
{
sbyte sb = 9;
sbyte nb = 3;
Console.WriteLine("~sb = " + ~sb);
Console.WriteLine("~sb+sb = " + (~sb + sb));
Console.WriteLine("~nb = " + ~nb);
Console.WriteLine("~nb+nb = " + (~nb + nb));
}
Результат выполнения программы:
~sb = -10
~sb+sb = -1
~nb = -4
~nb+nb = -1
Поразрядный сдвиг влево << целочисленного аргумента знакового типа может не только изменить его абсолютное значение, но и зачастую изменяет его знак. Приводить примеры здесь нет необходимости. Гораздо важнее рассмотреть особенности выполнения традиционных арифметических операций над беззнаковыми и знаковыми операндами с ограниченным количеством разрядов.
Начнем с беззнаковых целочисленных типов. В результате выполнения следующего фрагмента программы:
byte b=255, c=1, d;
d = b+c;
Значением переменной d будет 0. Обоснованность такого результата иллюстрирует следующее двоичное представление:
11111111 = 255
+
00000001 = 1
100000000 = 0 (нуль за счет отбрасывания левого разряда)
Теперь обратимся к операндам знаковых типов, например, типа sbyte.
Если просуммировать числа -1 (с поразрядным представлением 11111111) и 1 (с кодом 00000001), то получим девятиразрядное число с битовым представлением 100000000. Для внутреннего представления чисел типа sbyte отводится 8 разрядов. Девятиразрядное число в эти рамки не помещается, и левая (старшая) единица отбрасывается. Тем самым результатом суммирования становится код нуля 00000000. Все совершенно верно - выражение (-1 + 1) должно быть равно нулю! Однако так правильно завершаются вычисления не при всех значениях целочисленных операндов.
За счет ограниченной разрядности внутреннего представления значений целых типов при вычислении выражений с целочисленными операндами существует опасность аварийного выхода результата за пределы разрядной сетки. Например, после выполнения следующего фрагмента программы:
sbyte x=127, y=127, z;
z=(sbyte) (x+y);
Значением переменной z будет -2
В этом легко убедиться, представив выполнение операции суммирования в двоичном виде:
01111111 = 127
+ 01111111 = 127
11111110 = -2 (в дополнительном коде).
Примечание: В операторе z=(sbyte)(x+y); использована операция приведения типов (sbyte). При её отсутствии результат суммирования х+у автоматически приводится к типу int. Попытка присвоить значение типа int переменной z, имеющей тип sbyte, воспринимается как ошибка и компиляция завершается аварийно.
Приведенные иллюстрации переполнений разрядной сетки при арифметических операциях с восьмиразрядными целыми (типов byte, sbyte) могут быть распространены и на целые типы с большим количеством разрядов (эти типы с указанием разрядностей приведены в Табл. 2.1).
Основным типом для представления целочисленных данных в С# является тип int. Для представления целочисленных значений типа int используются 32-разрядные участки памяти. Тем самым предельные значения для значения типа int таковы:
положительные от 0 до 231-1;
отрицательные от -1 до -231.
В следующей программе результаты умножений переменной типа int на саму себя выходят за пределы разрядной сетки.
// 03_06.cs - переполнение при целочисленных операндах
static void Main06()
{
int m = 1001;
Console.WriteLine("m = " + m);
Console.WriteLine("m = " + (m = m * m));
Console.WriteLine("m = " + (m = m * m));
Console.WriteLine("m = " + (m = m * m));
}
В программе значение целочисленной переменной вначале равной 1001 последовательно умножается само на себя.
Результат выполнения программы:
m = 1001
m = 1002001
m = -1016343263
m = 554036801
После первого умножения m*m значением переменной m становится 1002001, после второго результат выходит за разрядную сетку из 32-х битов. Левые лишние разряды отбрасываются, однако, оставшийся самый левый 32-й бит оказывается равным 1, и код воспринимается как представление отрицательного числа. После следующего умножения 32-й бит оказывается равным 0, и арифметически неверный результат воспринимается как код положительного числа.
Особо отметим, что исполняющая система никак не реагирует на выход результата за разрядную сетку, и программисту нужно самостоятельно следить за возможностью появления таких неверных результатов.
В рассмотренных программах с переменными типов byte и sbyte мы несколько раз применили операцию преобразования (иначе приведения) типов. Например, были использованы конструкции:
(byte)(bb&dd)
z=(sbyte)(x+y);
В следующей главе приведение типов будет рассмотрено подробно, а сейчас покажем его роль в некоторых выражениях с целочисленными операндами.
Поместим в программу операторы:
short dd = 15, nn=24;
dd=(dd+nn)/dd;
При компиляции программы будет выведено сообщение об ошибке: Cannot implicitly convert type ‘int' to 'short'. Невозможно неявное преобразование типа int в short.
Несмотря на то, что в операторах использованы переменные только одного типа short, в сообщении компилятора указано, что появилось значение типа int! Компилятор не ошибся - при вычислении выражений с целочисленными операндами, отличными от типа long, они автоматически приводятся к типу int. Поэтому результат вычисления (dd+nn)/dd имеет тип int. Для значений типа short (см. табл. 2.1) выделяется два байта (16 разрядов), значение типа int занимает 4 байта. Попытка присвоить переменной dd с типом short значения типа int воспринимается компилятором как потенциальный источник ошибки за счёт потери 16-ти старших разрядов числа. Именно поэтому выдано сообщение об ошибке.
Программист может «успокоить» компилятор, применив следующим образом операцию приведения типов:
dd=(short)((dd+nn)/dd);
При таком присваивании программист берет на себя ответственность за правильность вычислений.
Обратите внимание на необходимость дополнительных скобок. Если записать (short)(dd+nn)/dd, то в соответствии с рангами операций к типу short будет приведено значение (dd+nn), а результат его деления на dd получит тип int.