Блог

Числа. От простого к сложному

Статья от нашего постоянного читателя о числах в Java.
14 марта 20188 минут4179

Наш постоянный читатель Кирилл Сергеев делится с нами особенностями хранения чисел в памяти компьютера, используя примеры из Java. В первой части статьи он ярко и сочно рассказывает, как числа влияли на человечество на заре веков, а во второй объясняет, в каких форматах память компьютера хранит разные виды чисел. Желаем приятного чтения!

Числа сквозь века: мифы, легенды, развитие

Люди на заре человечества создали огромное количество удивительных изобретений. Простых, может быть, даже примитивных. Но удивительно полезных. Сумевших не просто вывести человека на вершину пищевой цепочки, но и сделать его именно тем, кем он является ныне. Палка как примитивное орудие и отесанный камень вместе превратились в грозное оружие. Человек подчинил огонь и покорил воду. Человек приручил животных. Человек заговорил. А главное – стал использовать числа.

Сначала числа использовались исключительно для определения количества тех или иных однотипных предметов. Это были натуральные числа – как и предметы, счет которым с их помощью вели. Три шкуры, пять наконечников для стрел, два топора. Один, два, три, …, 15 членов общины, а дальше – «нас тьмы, и тьмы, и тьмы. Попробуйте, сразитесь с нами!».

Для нужд первобытного человека таких чисел хватало вполне. Но прогресс не удержать. Люди научились обменивать «что-нибудь ненужное» на предметы, необходимые в быту. А когда зародилась примитивная торговля, тут же появились профессиональные менялы и ростовщики. Чтобы начать охотиться, человек сначала должен где-то получить топор, копье или гарпун. Старшие товарищи уже обладают этими предметами. Так почему не одолжить у них орудие труда в счет будущей добычи или улова? Люди научились давать и брать в долг. Но если вы должны кому-то три беличьи шкурки, а у вас нет ни одной – получается, у вас минус три беличьи шкурки. Сначала не было топора, не было и шкурок. То есть – «все по нулям». Теперь – один топор и минус три охотничьих трофея. Люди научились считать количество целых неделимых предметов: как положительное, так и отрицательное. Так появились целые числа.

Человек развивался, менялись язык и представление о числах. Школ и гимназий еще не было – опыт поколений передавался из уст в уста. В том числе и с помощью сказок. Давайте вспомним и мы одну. Про лису и медведя.

Хитрая лиса поселилась у медведя и тайком по вечерам «хавала его ништяки» – опорожняла кадку с медом. Медведю она говорила, что уходит то на родины, то на крестины. А сама забиралась на чердак и лакомилась медком. В первый вечер она съела четверть кадушки и вернулась в дом. Медведь поинтересовался, как ребенка назвали. Лиса и говорит: «Верхушечкой». Во второй вечер лиса слямзила еще четверть кадки, осталась половина. Лиса вернулась в дом и на вопрос медведя ответила, что ребенка назвали Середочкой. В третий вечер лиса разъелась до того, что навернула всю оставшуюся половину меда. Медведю с гордостью сообщила, что ребенка окрестили Поскребушком. И то правда, мало ли чудных имен на свете?

Сказка – ложь, да в ней намек… Лиса не просто объедала медведя. Делала она это «дискретно», в три подхода, каждый раз съедая часть целого. Сначала она осилила четверть кадки, после этого осталось три четверти. Во второй раз лиса вновь съела четверть. Кадушка стала наполовину пустой. А может быть, наполовину полной. Как бы то ни было, осталась половина. В третий раз лиса до того распробовала медок, что съела всю оставшуюся половину разом. В результате лисьей «рационализации» целая кадка натурального меда превратилась в ноль. Люди тем временем освоили рациональные дроби.

Человек рос. Росли города. Развивались цивилизации: шумерская, египетская, древнегреческая. И такие там жили люди, что хлебом их не корми, а дай построить зиккурат, пирамиду или храм Артемиды. Касательно храма Артемиды – если не построить, то уж хотя бы сжечь.

Для создания всего этого великолепия древние строители пользовались «золотым сечением», извлекали квадратные корни, выводили на все лады число Пи. Почти всякий раз они сталкивались с бесконечными непериодическими десятичными дробями. Они казались им настолько чудными и отличными от рациональных дробей, что они их так и назвали – иррациональные числа. Правда, дальше этого древние геометры не пошли.

Только в Новое и Новейшее время появляется строгая теория вещественных чисел. Множество вещественных чисел, кроме рациональных, включает множество иррациональных чисел.

Сегодня человек при операциях с числами использует не единичную систему счисления, пальцы на руках и ногах, набор косточек или камушков, не устный счет, не абак, не хитроумные механические счетные машины. Компьютер теперь – не роскошь, а средство вычисления. Предлагаю рассмотреть, как хранятся числа в памяти компьютера. Особый упор сделаем на рассмотрение формата хранения вещественных чисел. Посмотрим, как это делается, и разберем примеры на современном, ультрановомодном и востребованном языке программирования высокого уровня – Java.

Как хранятся числа в памяти компьютера

Формат представления целых чисел

С целыми положительными числами все предельно просто. Выделяется n-бит на число. Число переводится в двоичную систему счисления. Затем записывается последовательно с нулевого бита по n-1 бит. Старшие не значащие разряды обнуляются.

Рассмотрим пример. Пусть есть целое число 389. Как определить, в каком формате хранится это число?

Решение:

Переведем число 38910 в двоичную систему счисления.

38910 = 1100001012

Так как тип целочисленный, под его хранение отводится четыре байта или 32 бита. Таким образом, ответ может быть записан так:

31                                                         0

00000000000000000000000110000101

Для проверки результата выполним небольшой код на Java:

int i = 389;
String intBits = Integer.toBinaryString(i);
System.out.println("Разряды числа: " + intBits);

Результат работы программы:
Разряды числа: 110000101

Результат вычислений совпал с результатом работы программы.

Формат хранения целых отрицательных чисел уже интересней. Отрицательное целое число представлено в дополнительном коде. Для перевода числа в дополнительный код нужно перевести его в двоичную систему счисления. Результат перевода представить в обратном коде. Для этого нужно поразрядно заменить все «0» на «1», а «1» на «0». К полученному результату нужно прибавить «1».

Рассмотрим пример. Дано целое число -386. Как определить, в каком формате хранится это число?

Решение:

Переведем число 38610 в двоичную систему счисления.

38610 = 000000000000000000000001100000102

Представим результат перевода в обратном коде.

Обр. код: 11111111111111111111111001111101

Представим результат в дополнительном коде.

Доп. код: 11111111111111111111111001111101 + 1 =

= 11111111111111111111111001111110

Так как тип целочисленный, под его хранение отводится четыре байта или 32 бита. Таким образом, ответ может быть записан в следующем виде:

31                                                  0

11111111111111111111111001111110

Для проверки результата выполним небольшой код на Java:

int i = -386;
String intBits = Integer.toBinaryString(i);
System.out.println("Разряды числа: " + intBits);

Результат работы программы:
Разряды числа: 11111111111111111111111001111110

Результат вычислений совпал с результатом работы программы.

Формат представления вещественных чисел

Вещественные числа в Java хранятся в формате чисел с плавающей точкой, в которой число представлено в виде мантиссы и степени базы старшего разряда. Например, 75.3810 может быть записано в следующих видах:

0.7538 * 100 = 0.7538 * 102, здесь мантисса – 0.7538, база (основание системы счисления) – 10, степень старшего разряда (разряд десятков) – 2,

0.007538 * 10000 = 0.007538 * 104,

7538.0 / 100 = 7538 * 10-2.

В общем виде число с плавающей запятой состоит из знака мантиссы, знака порядка, порядка и мантиссы. Знак мантиссы определяет, больше нуля или меньше. Знак порядка показывает, в каком направлении смещается точка, а порядок определяет, на сколько знаков смещается точка. Наконец, мантисса представляет само число.

S*M*BQ,

где

S – знак числа (мантиссы);

M – мантисса числа;

B – основание системы счисления, у нас 10;

Q – порядок числа.

Приведенный пример демонстрирует, как точка перемещается вдоль цифр числа. Точка мечется, как стрелка осциллографа. Очевидно, что таких представлений может быть столько, на сколько хватит фантазии.

Такое положение дел никак не может устроить программистов и инженеров, разрабатывающих электронно-вычислительную аппаратуру и программы для нее. Представление чисел с плавающей точкой должно быть унифицировано и стандартизировано. Таким стандартом является IEEE 754. Этот стандарт предусматривает, что число всегда хранится в нормализованной форме. Для чисел, представленных в двоичном коде, это означает, что точка будет сдвигаться влево или вправо до тех пор, пока в старшем бите мантиссы не окажется «1». При этом «1» в мантиссу не записывается. Она становится «неявной». Делается это для экономии одного разряда. Аппаратные средства устроены так, что они сами «помнят» о ее существовании и действуют с мантиссой так, как будто она там есть. То есть, мантисса будет иметь вид 1.M.

Показатель степени хранится в виде целого числа в коде со сдвигом 1023 или 127, зависит от точности. Это означает, что сдвиг точки для числа с двойной точностью хранится не в виде +2, -1, +6, а в виде 2+1023, -1+1023, 6+1023. Поэтому он всегда положительный. А оборудование само вычисляет, на сколько порядков переместить точку в мантиссе, и определяет направление сдвига. Разберем вышесказанное на примере.

Представим число 75.3810 в формате представления числа с плавающей точкой двойной точности. В Java это тип double. Построим это представление, исходя из определений и стандарта IEEE 754. В общем виде формат представления будет таким:

63

62                                     52

51                                       0

Знак

Порядок

Мантисса

Под все число отводится 64 бита. Под знак – 1 бит. Под порядок – 11 бит. Под мантиссу – 52 бита.

Сначала переведем число 75.3810 в двоичную систему счисления. Перевод целой и дробной части осуществляется по-разному. Целая часть получается путем деления ее на 2 и записи остатков от деления в порядке, обратном их возникновению. Дробная часть получается путем ее умножения на 2 и записи целых частей в порядке их возникновения.

7510 = 10010112

0.3810 = 01100001010001111010111000010100011110101110002

75.3810 = 1001011.01100001010001111010111000010100011110101110002

Теперь нужно сдвинуть целую часть вправо так, чтобы в целой части осталась одна единица. Эта единица неявная. Она не будет записана в мантиссу числа.

Получим:

1.M = 1.0010110110000101000111101011100001010001111010111000,

точка сдвинулась на шесть разрядов влево.

M = 0010110110000101000111101011100001010001111010111000

Представление мантиссы получили. Дело за малым – получить представление порядка. Помним, что порядок хранится в коде со сдвигом 1023. Поэтому, если мы сдвигали точку влево на шесть разрядов, мы должны вычислить выражение

610 + 102310 = 102910

и перевести результат в двоичную систему счисления.

Q = 102910 = 100000001012

Со знаком все просто. Число у нас положительное, поэтому

S = 0.

Запишем результат:

63

62              52

51                                                                                              0

0

10000000101

0010110110000101000111101011100001010001111010111000

Теперь воспроизведем полученный результат программно. Для этого выполним следующий код:

double d = 75.38;
String sResult = "";
long numberBits = Double.doubleToLongBits(d);

sResult = Long.toBinaryString(numberBits);
System.out.println("Представление вещественного числа в формате чисел с плавающей точкой");

System.out.format("Число: %5.2f\n", d);
System.out.println("Формат чисел с плавающей точкой:");

//ведущий ноль заботливо сокращен системой, поэтому его нужно восстановить
System.out.println(d > 0 ? "0" + sResult : sResult);

Результат работы программы:
Представление вещественного числа в формате чисел с плавающей точкой
Число: 75,38
Формат чисел с плавающей точкой:
0100000001010010110110000101000111101011100001010001111010111000

Результат вычислений совпал с результатом работы программы.

Рассмотрим пример. Для закрепления представим число Пи в форме представления чисел с плавающей точкой.

Решение:

Переведем Пи из десятичной системы счисления в двоичную.

3,14159265358979310 = 11.0010010000111111011010101000100010000101101000110002

Число положительное – значит, знак равен 0.

Определяем мантиссу. В старшем разряде должна остаться одна 1. Поэтому нужно сдвинуть точку на один разряд влево.

1.M = 1.1001001000011111101101010100010001000010110100011000

M = 1001001000011111101101010100010001000010110100011000

Теперь определим порядок. Точка была смещена на один разряд влево. Получаем

Q = 1 + 1023 = 102410 = 100000000002

Запишем результат:

63

62             52

51                                                                                             0

0

10000000000

100100100001111110110101010001000100001011010001100

Воспроизведем полученный результат программно. Для этого выполним следующий код:

double d = Math.PI;
String sResult = "";
long numberBits = Double.doubleToLongBits(d);

sResult = Long.toBinaryString(numberBits);
System.out.println("Представление вещественного числа в формате чисел с плавающей точкой");

System.out.format("Число: %10.15f\n", d);
System.out.println("Формат чисел с плавающей точкой:");

//ведущий ноль заботливо сокращен системой, поэтому его нужно восстановить
System.out.println(d > 0 ? "0" + sResult : sResult);

Результат работы программы:
Представление вещественного числа в формате чисел с плавающей точкой
Число: 3,141592653589793
Формат чисел с плавающей точкой:
0100000000001001001000011111101101010100010001000010110100011000

Результат вычислений совпал с результатом работы программы.

Вместо выводов и заключения

Запустим на выполнение небольшой код и посмотрим на результат.

double d = 5.0/7;
System.out.format("Число: %10.16f\n", d);

d += 300000;
System.out.format("Число: %10.16f\n", d);

Результат работы программы
Число: 0,7142857142857143
Число: 300000,7142857142600000

Что стало с точностью? Откуда эти нули? Ее вытеснила целая часть. Перемещаясь вправо, целая часть вытеснила из мантиссы младшие разряды. Ведь всего под нее предоставлено 52 бита. Поэтому младшие разряды просто вытолкнуло за правую границу мантиссы. Точность оказалась за нулевым разрядом. Таким образом, очевидно, что представление вещественных чисел в формате чисел с плавающей точкой – это компромисс между точностью и диапазоном представляемых значений.

java_developerjavaпрограммированиематематика
Нашли ошибку в тексте? Напишите нам.
Спасибо,
что читаете наш блог!
Posts popup