М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 9
Текст из файла (страница 9)
В математике для представления чисел со знаком используется специальный символ «-», за которым следует обычная запись абсолютного значения числа. Компьютеру с таким представлением работать неудобно. Поэтому большинство компьютеров представляет целые числа со знаком в записи, называющейся дополнением до двух *. Положительное число представляется старшим нулевым битом и следующим за ним обычным двоичным представлением значения. Из этого вытекает, что самое большое положительное целое число, которое может быть представлено словом из w битов, не превышает 2W-1 - 1.
Для того чтобы получить представление числа -п по двоичному представлению В = b1b2...bwчисла n:
• берут логическое дополнение В, т. е. заменяют в каждом b ноль на единицу, а единицу на ноль,
• прибавляют единицу.
Например, представление -1, -2 и -127 в виде 8-разрядных слов получается так:
У отрицательных значений в старшем бите всегда будет единица.
Дополнение до двух удобно тем, что при выполнении над такими представлениями операций обычной двоичной целочисленной арифметики получается правильное представление результата:
(-!)-! = -2
1111 1111-00000001 = 1111 1110
Отметим, что строку битов 1000 0000 нельзя получить ни из какого положительного значения. Она представляет значение -128, тогда как соответствующее положительное значение 128 нельзя представить как 8-разрядное число. Необходимо учитывать эту асимметрию в диапазоне типов integer, особенно при работе с типами short.
Альтернативное представление чисел со знаками — дополнение до единицы, в котором представление значения -n является просто дополнением п. В этом случае набор значений симметричен, но зато есть два представления для нуля: 0000 0000 называется положительным нулем, а 1111 1111 называется отрицательным нулем.
Если в объявлении переменной синтаксически не указано, что она без знака (например, unsigned), то по умолчанию она считается целой со знаком:
I
nt i; /* Целое со знаком в языке С */
I: Integer; -- Целое со знаком в языке Ada
Целочисленные операции
К целочисленным операциям относятся четыре основных действия: сложение, вычитание, умножение и деление. Их можно использовать для составления выражений:
а + b/с - 25* (d - е)
К целочисленным операциям применимы обычные математические правила старшинства операций; для изменения порядка вычислений можно использовать круглые скобки.
Результат операции над целыми числами со знаком не должен выходить за диапазон допустимых значений, иначе произойдет переполнение, как рассмотрено ниже. Для целых чисел без знака используется циклическая арифметика. Если short int хранится в 16-разрядном слове, то:
с |
unsigned short int i; /* Диапазон i= 0...65535*/
i = 65535; /* Наибольшее допустимое значение*/
i = i + 1; /*Циклическая арифметика, i = 0 */
Разработчики Ada 83 сделали ошибку, не включив в язык целые без знака. Ada 95 обобщает концепцию целых чисел без знака до модульных типов, которые являются целочисленными типами с циклической арифметикой по произвольному модулю. Обычный байт без знака можно объявить как:
Ada |
type Unsigned_Byte is mod 256;
тогда как модуль, не равный двум, можно использовать для хеш-таблиц или случайных чисел:
Ada |
Обратите внимание, что модульные типы в языке Ada переносимы, так как частью определения является только циклический диапазон, а не размер представления, как в языке С.
Деление
В математике в результате деления двух целых чисел а/b получаются два значения: частное q и остаток r, такие что:
а = q * b + r
Так как результатом арифметического выражения в программах является единственное
значение, то для получения частного используют оператор «/», а для получения остатка применяют другой оператор (в языке С это «%», а в Ada — rem). Выражение 54/10 дает значение 5, и мы говорим, что результат операции был усечен (truncated). В языке Pascal для целочисленного деления используется специальная операция div.
При рассмотрении отрицательных чисел определение целочисленного деления не столь тривиально. Чему равно выражение -54/10: -5 или -6? Другими словами, до какого значения делается усечение: до меньшего («более отрицательного») или до ближайшего к нулю? Один вариант — это усечение в сторону нуля, поскольку, чтобы удовлетворить соотношение для целочисленного деления, достаточно просто сменить знак остатка:
-54 = -5*10 + (-4)
Однако существует и другая математическая операция, взятие по модулю (modulo), которая соответствует округлению отрицательных частных до меньшего («более отрицательного») значения:
-54 = -6* 10+ 6
-54 mod 10 = 6
Арифметика по модулю используется всякий раз, когда арифметические операции выполняются над конечными диапазонами, например над кодами с исправлением ошибок в системах связи.
Значение операций «/» и «%» в языке С зависит от реализации, поэтому программы, использующие эти целочисленные операции, могут оказаться непереносимыми. В Ada операция «/» всегда усекает в сторону нуля. Операция rem возвращает остаток, соответствующий усечению в сторону нуля, в то время как операция mod возвращает остаток, соответствующий усечению в сторону минус бесконечности.
Переполнение
Говорят, что операция приводит к переполнению, если она дает результат, который выходит за диапазон допустимых значений. Следующие рассуждения для ясности даются в терминах 8-разрядных целых чисел.
Предположим, что переменная i типа signed integer имеет значение 127 и что мы увеличиваем i на 1. Компьютер просто прибавит единицу к целочисленному представлению 127:
0111 1111+00000001 = 10000000
и получит -128. Это неправильный результат, и ошибка вызвана переполнением. Переполнение может приводить к странным ошибкам:
C |
for (i = 0; i < j*k; i++)….
Если происходит переполнение выражения j*k, верхняя граница может оказаться отрицательной и цикл не будет выполнен.
Предположите теперь, что переменные a, b и с имеют значения 90, 60 и 80, соответственно. Выражение (а - b + с) вычисляется как 110, потому что (а - b) дает 30, и затем при сложении получается 110. Однако оптимизатор для вычисления выражения может выбрать другой порядок, (а + с - b), давая неправильный ответ, потому что сложение (а + с) дает значение 170, что вызывает переполнение. Если вам в средней школе говорили, что сложение является коммутативным и ассоциативным, то речь шла о математике, а не о программировании!
В некоторых компиляторах можно задать режим проверки на переполнение каждой целочисленной операции, но это может привести к чрезмерным издержкам времени выполнения, если обнаружение переполнения не делается аппаратными средствами. Теперь, когда большинство компьютеров использует 32-разрядные слова памяти, целочисленное переполнение встречается редко, но вы должны знать о такой возможности и соблюдать осторожность, чтобы не попасть в ловушки, продемонстрированные выше.
Реализация
Целочисленные значения хранятся непосредственно в словах памяти. Некоторые компьютеры имеют команды для вычислений с частями слов или даже отдельными байтами. Компиляторы для этих компьютеров обычно помещают short int в часть слова, в то время как компиляторы для компьютеров, которые распознают только полные слова, реализуют целочисленные типы int и short int одинаково. Тип long int обычно распределяется в два слова, чтобы получить больший диапазон значений.
Сложение и вычитание компилируются непосредственно в соответствующие команды. Умножение также превращается в одну команду, но выполняется значительно дольше, чем сложение и вычитание. Умножение двух слов, хранящихся в регистрах R1 и R2, дает результат длиной в два слова и требует для хранения двух регистров. Если регистр, содержащий старшее значение, — не ноль, то произошло переполнение.
Для деления требуется, чтобы компьютер выполнил итерационный алгоритм, аналогичный «делению в столбик», выполняемому вручную. Это делается аппаратными средствами, и вам не нужно беспокоиться о деталях, но, если для вас важна эффективность, деления лучше избегать.
Арифметические операции выполняются для типа long int более чем вдвое дольше, нежели операции для int. Причина в том, что нужны дополнительные команды для «распространения» переноса, который может возникать, из слова младших разрядов в слово старших.
4.2. Типы перечисления
Языки программирования типа Fortran и С описывают данные в терминах компьютера. Данные реального мира должны быть явно отображены на типы данных, которые существуют на компьютере, в большинстве случаев на один из целочисленных типов. Например, если вы пишете программу для управления нагревателем, вы могли бы использовать переменную dial для хранения текущей позиции регулятора. Предположим, что реальная шкала имеет четыре позиции: off (выключено), low (слабо), medium (средне), high (сильно). Как бы вы объявили переменную и обозначили позиции? Поскольку компьютер не имеет команд, которые работают со словами памяти, имеющими только четыре значения, для объявления переменной вы выберете тип integer, а для обозначения позиций четыре конкретных целых числа (скажем 1, 2, 3, 4):
C |
int dial; /* Текущая позиция шкалы */
if (dial < 4) dial++; /* Увеличить уровень нагрева*/
Очевидно, что при использовании целых чисел программу становится трудно читать и поддерживать. Чтобы понять код программы, вам придется написать обширную документацию и постоянно в нее заглядывать. Чтобыулучшить программу, можно прежде всего задокументировать подразумеваемые значения внутри самой программы:
#define Off 1
C |
#define Medium 3
#define High 4
int dial;
if(dial<High)dial++;
Однако улучшение документации ничего не дает для предотвращения следующих проблем:
C |
dial = High + 1; /* Нераспознаваемое переполнение*/
dial = dial * 3; /* Бессмысленная операция*/
Другими словами, представление шкалы с четырьмя позициями в виде целого числа позволяет программисту присваивать значения, которые выходят за допустимый диапазон, и выполнять команды, бессмысленные для реального объекта. Даже если программист не создаст преднамеренно ни одну из этих проблем, опыт показывает, что они часто появляются в результате отсутствия I взаимопонимания между членами группы разработчиков программы, опеча- ток и других ошибок, типичных при создании сложных систем.
Решение состоит в том, чтобы разрешить разработчику программы созда- вать новые типы, точно соответствующие тем объектам реального мира, которые нужно моделировать. Рассматриваемая здесь короткая упорядоченная последовательность значений настолько часто встречается, что современные языки программирования поддерживают создание типов, называемых типами — перечислениями (enumiration types)*. В языке Ada вышеупомянутый пример выглядел бы так:
Ada type Heat is (Off, Low, Medium, High);
Dial: Heat;
Ada |
if Dial < High then Dial := Heat'Succ(DialJ;
Dial:=-1; —Ошибка
Dial := Heat'Succ(High); -- Ошибка
Dial := Dial * 3; - Ошибка
Перед тем как подробно объяснить пример, обратим внимание на то, что в языке С есть конструкция, на первый взгляд точно такая же:
C |
typedef enum {Off, Low, Medium, High} Heat;
Однако переменные, объявленные с типом Heat, — все еще целые, и ни одна из вышеупомянутых команд не считается ошибкой (хотя компилятор может выдавать предупреждение):
Heat dial;
C |
dial = High + 1; /* Не является ошибкой! */
dial = dial * 3; /* Не является ошибкой! */