Как хранится float в памяти c
В С++ существуют два типа данных с плавающей точкой: float и double . Типы данных с плавающей точкой предназначены для хранения чисел с плавающей точкой. Типы данных float и double могут хранить как положительные, так и отрицательные числа с плавающей точкой. У типа данных float размер занимаемой памяти в два раза меньше, чем у типа данных double , а значит и диапазон принимаемых значений тоже меньше. Если тип данных float объявить с приставкой long , то диапазон принимаемых значений станет равен диапазону принимаемых значений типа данных double . В основном, типы данных с плавающей точкой нужны для решения задач с высокой точностью вычислений, например, операции с деньгами.
Как хранятся действительные числа в компьютере
Для хранения действительных чисел в памяти компьютера отводится определённое количество бит. Действительное число хранится в виде знака (плюс или минус), мантиссы и экспоненты. Что такое мантисса и экспонента лучше объяснить на примере: масса Земли равна 5.972*10 24 килограмм. Здесь 5.972 — мантисса, а 24 — экспонента.
При выводе больших (или очень маленьких) чисел в программе на C++ можно увидеть на экране запись типа 5.972E23. Сначала выводится мантисса, затем — буква E, а затем — экспонента. Запись представлена в десятичной системе счисления. В таком же формате можно вводить большие или очень маленькие действительные числа. Этот формат называется экспоненциальной записью числа.
Мы будем работать с типом double (с числами двойной точности), который занимает 8 байт. Один бит отводится под знак числа, 11 под экспоненту и 52 под мантиссу. С помощью 52 бит можно хранить числа длиной до 15-16 десятичных цифр. Таким образом, независимо от того, какая у числа экспонента, правильные значения будут иметь только первые 15 цифр. В примере с массой Земли точно заданы первые 4 цифры, таким образом, погрешность составляет 10 20 килограмм. Это довольно большая погрешность. Чтобы масса Земли с точностью до первых четырёх знаков изменилась, на неё нужно дополнительно поселить миллиард миллиардов довольно упитанных людей.
Таким образом, можно сказать, что числа в компьютере хранятся не с абсолютной, а с относительной погрешностью (то есть погрешность зависит от значения хранимого числа).
То, что числа хранятся неточно, создаёт нам множество проблем.
Итак, мы рассмотрели главные моменты, касающиеся основных типов данных в С++. Осталось только показать, откуда взялись все эти диапазоны принимаемых значений и размеры занимаемой памяти. А для этого разработаем программу, которая будет вычислять основные характеристики всех, выше рассмотренных, типов данных.
На этом уроке мы рассмотрим типы данных с плавающей точкой в языке С++, их точность и диапазон. Выясним, что такое экспоненциальная запись и как она используется, а также рассмотрим ошибки округления и дадим определения для nan и inf .
Типы данных с плавающей точкой
Есть три типа данных с плавающей точкой: float, double и long double. Язык C++ определяет только их минимальный размер (как и с целочисленными типами). Типы данных с плавающей точкой всегда являются signed (т.е. могут хранить как положительные, так и отрицательные числа).
Тип | Минимальный размер | Типичный размер | |
Тип данных с плавающей точкой | float | 4 байта | 4 байта |
double | 8 байт | 8 байт | |
long double | 8 байт | 8, 12 или 16 байт |
Объявление переменных разных типов данных с плавающей точкой:
Если нужно использовать целое число с переменной типа с плавающей точкой, то тогда после этого числа нужно поставить разделительную точку и нуль. Это позволяет различать переменные целочисленных типов от переменных типов с плавающей запятой:
double d ( 5.0 ) ; // 5.0 - это тип данных с плавающей точкой (по умолчанию double) float f ( 5.0f ) ; // 5.0 - это тип данных с плавающей точкой ("f" от "float")Обратите внимание, литералы типа с плавающей точкой по умолчанию относятся к типу double. f в конце числа означает тип float.
Экспоненциальная запись
Обычно, в экспоненциальной записи, в целой части находится только одна цифра, все остальные пишутся после разделительной точки (в дробной части).
Рассмотрим массу Земли. В десятичной системе счисления она представлена как 5973600000000000000000000 кг . Согласитесь, очень большое число (даже слишком большое, чтобы поместиться в целочисленную переменную размером 8 байт). Это число даже трудно читать (там 19 или 20 нулей?). Но используя экспоненциальную запись, массу Земли можно представить, как 5.9736 × 10 24 кг (что гораздо легче воспринимается, согласитесь). Еще одним преимуществом экспоненциальной записи является сравнение двух очень больших или очень маленьких чисел — для этого достаточно просто сравнить их экспоненты.
В языке C++ буква е / Е означает, что число 10 нужно возвести в степень, которая следует за этой буквой. Например, 1.2 × 10 4 эквивалентно 1.2e4 , значение 5.9736 × 10 24 еще можно записать как 5.9736e24 .
Для чисел меньше единицы экспонент может быть отрицательным. Например, 5e-2 эквивалентно 5 * 10 -2 , что, в свою очередь, означает 5 / 10 2 или 0.05 . Масса электрона равна 9.1093822e-31 кг .
На практике экспоненциальная запись может использоваться в операциях присваивания следующим образом:
Подпишитесь на мой телеграм-канал, там я пишу о дотнете и веб-разработке.
Поехали
В этой статье я разбираю, как Double хранится в памяти, но это не самостоятельная статья, в том плане, что вряд ли вы сможете разобраться с этой темой, прочитав одну её, поэтому я порекомендую ссылки на другие статьи (хотя и их тоже не назовёшь идеальными).
Также в процессе подготовки этого материала я нашёл статью на русском: Взгляд со стороны: Стандарт IEEE754 - она в целом о стандарте хранения чисел IEEE754, статья основательная, но довольно абстрактная и её не назовёшь лёгким, доступным материалом.
В этой же статье я больше сосредоточюсь на просмотре конкретных байтовых представлениях чисел в памяти, разобрав как они получаются. Мне кажется, таких конкретных примеров не хватает в статьях выше для лучшего понимания.
Кратко о том как Double хранится в памяти
Double занимает в памяти 8 байт или 64 разряда в двоичном представлении. Старший разряд хранит знак числа - 0 обозначает положительное число, а 1 отрицательное. 11 следующих разрядов занимает часть, которую называют экспонента. Оставшиеся 52 - часть называемая мантисса. Комбинация этих трёх компонент в виде: знак * мантисса * 2 ^ экспонента , с небольшой предварительной манипуляцией над этими компонентами и будет представлять хранимое число. Есть разные классы хранимых чисел: нормализованные, субнормальные, бесконечность и Nan - они отличаются тем, как именно из хранимых компонент получается итоговое число.
Как можно посмотреть байтовое представление Double
Класс BitConverter позволяет получить байтовое представление базовых типов или наоборот преобразовать байтовое представление в базовый тип.
Double занимает 8 байтов в памяти и метод GetBytes возвращает нам массив из 8 элементов - всё сходится.
Но для разбора удобнее числа представлять в двоичной системе исчисления. Можно для этого каждый элемент массива представить в двоичном виде.
А можно воспользоваться методом DoubleToInt64Bits - он возвращает 64 битное целое число, которое в двоичном виде соответствует байтовому представлению числа типа double.
Для разобра удобнее сразу разделить число на его основные составляющие: мантиссу, экспоненту и знак.
Как получаются нормализованные числа
Нормализованное число - это, по сути, способ кодирования в мантиссе и экспоненте чисел представленных типом Double и лежащих в определённом числовом диапазоне (всех кроме специальных и очень маленьких). Формула для получения итогового числа: М * 2 ^ е , где M - мантисса, а e - экспонента, но чтобы получить Мантиссу и Экспоненту из данных непосредственно хранимых в Double, нужно проделать некоторые манипуляции над ними.
В Double для мантиссы отводятся 52 разряда, ещё один старший разряд в нормализованных числах подразумевается и он всегда равен 1. Чтобы из мантиссы хранимой в Double получить настоящую мантиссу, которую нужно умножать на экспоненту, нужно добавить к ней ещё один разряд заполненный 1 и разделить на 2 в степени 52.
Например для числа 1.0d хранимая мантисса (мы её получили в примере выше): 0000000000000000000000000000000000000000000000000000 - это 52 разряда заполненные нулями, при добавлении ещё одного подразумеваемого разряда заполненного единицей получается: 10000000000000000000000000000000000000000000000000000 - это уже 53 разряда с единицей в старшем разряде, при делении этого числа на 2^52 результат будет 1.0000000000000000000000000000000000000000000000000000 в двоичной системе исчисления (и в данном случае, точно такой же в десятичной - 1.0) - при делении на 2^52 мы двигаем двоичное число вправо на 52 разряда за точку целого числа в итоге мантисса это всегда число вида: 1.xxx. - в двоичной системе исчисления, где “xxx…” часть хранится в мантиссе Double. При переводе в десятичную систему исчисления получается, что мантисса всегда лежит в диапазоне: 1 <= M < 2
Теперь поговорим о том, как из хранимой экспоненты, получить настоящую экспоненту.
Для хранения экспоненты в Double отводится 11 разрядов - 11 разрядов дают нам 2^11 = 2048 числа с учётом 0 (то есть наибольшее хранимое число это 2047), которые мы можем использовать в качестве экспоненты. Но так как мантисса у нас всегда лежит в диапазоне от 1 до 2, а с помощью нормализованных чисел мы представляем как числа по модулю и большие 2 и меньшие 1, то нам необходимо в этих 11 разрядах хранить также отрицательные экспоненты, которые позволят получить числа меньше 1. Для этого подразумевается, что хранимая экспонента - это сдвиг относительно числа 1023. Примеры:
- Если в экспоненте Double хранится число 1023, то значит реальная экспонента: 1023 - 1023 = 0 .
- Если в экспоненте Double хранится число 1024, то значит реальная экспонента: 1024 - 1023 = 1 .
- Если в экспоненте Double хранится число 1022, то значит реальная экспонента: 1022 - 1023 = -1 .
- Если в экспоненте Double хранится число 0, то значит реальная экспонента… А вот так нельзя, 0 зарезервированное число и в нормализованных числах хранимая экспонента никогда не будет 0 - наименьшая хранимая экспонента - это 1, а значит наименьшая реальная экспонента 1 - 1023= -1022 . Но это только в нормализованных числах, если экспонента всё-таки равна 0, то значит кодируемое число либо 0, либо субнормальное число.
- Если в экспоненте Double хранится число 2047 (максимальное возможное число для 11 разрядов), то значит реальная экспонента… А вот так тоже нельзя, 2047 тоже зарезервированное число - наибольшая хранимая экспонента нормализованного числа - это 2046, а значит наибольшая реальная экспонента 2046 - 1023 = 1023 . Если вы всё-же видите в экспоненте 2047, то значит кодируемое число либо Nan, либо Infinity.
Теперь мы знаем как получить мантиссу и экспоненту из тех данных, что хранит Double. И мы можем посчитать самое большое и самое маленькое число, которое может хранится в нормализованном числе.
Самое большое число:
- Самая большая реальная мантисса: 1.11111111 11111111 11111111 11111111 11111111 11111111 1111 или в десятичном виде 9 007 199 254 740 991 (53 разряда заполненные единицами) / 2^52 = 1.9999999999999997779553950749687
- Самая большая реальная экспонента: 1022
- Самое большое хранимое нормализованное число по нашим расчётам: 1.9999999999999997779553950749687 * 2^1022 = 1,797693134862315708145274237317e+308
- И по официальным данным: 1.7976931348623157e+308
И это в принципе самое больше число, которое может хранится в Double, так как другие классы чисел не позволяют хранить числа большие, чем может хранить нормализованное число.
Самое маленькое положительное число:
- Самая маленькая реальная мантисса: 1.0000000000000000000000000000000000000000000000000000
- Самая маленькая реальная экспонента: -1022
- Самое маленькое позитивное нормализованное число по нашим расчётам: 1.0 * 2^-1022 = 2,2250738585072013830902327173324e-308
- И по официальным данным: 2.2250738585072010e-308
Если нужно будет хранить число меньше, то это будет сделано используя другой способ хранения числа, которое называется “субнормальное число”.
Любое нормализованное число из выше определённого диапазона нормализованных чисел может быть сделано отрицательным с помощью изменения старшего разряда в единицу в 64 разрядах типа данных Double.
Разберём некоторые нормализованные числа
Я написал сниппет, с помощью которого можно посмотреть разбор любого нормализованного числа:
Реальная экспонента - 0, так как само число уже лежит в диапазоне от 1 до 2, поэтому можно сразу же сохранить мантиссу, откинув целую 1.
Отличается от 1.0d только знаком в старшем разряде Double.
Число тоже лежит в диапазоне между 1 и 2, поэтому реальная экспонента будет 0.
Попробуем сами получить хранимую мантиссу:
1.1 * 2^52 = 4 953 959 590 107 545.6 округляем до 4 953 959 590 107 546 в двоичном виде: 1 0001 10011001 10011001 10011001 10011001 10011001 10011010 отбрасываем старший разряд и получаем искомое 0001100110011001100110011001100110011001100110011010
Попробуем сами получить хранимую мантиссу и экспоненту:
Сначала нам нужно привести число к виду: М * 2^e - так чтобы M лежала между 1 и 2. Это будет: 1,9073486328125 * 2^19 .
Теперь нам известна реальная экспонента - 19, для того, чтобы получить хранимую нужно к 1023 прибавить 19 итого получается: 1042 - это хранимая экспонента.
Нам также известна реальная мантисса - это 1,9073486328125 . Для того, чтобы получить хранимую мантиссу умножим её на 2^52: 1,9073486328125 * 2^52 = 8 589 934 592 000 000 . В двоичном виде: 1 1110 1000 0100 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 . Отбросим старший разряд и получим хранимую мантиссу: 1110 1000 0100 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 .
Попробуем сами получить хранимую мантиссу и экспоненту:
Сначала нам нужно привести число к виду: М * 2^e - так чтобы M лежала между 1 и 2. Это будет: 1,6 * 2^-4 .
Теперь нам известна реальная экспонента - это -4, для того, чтобы получить хранимую нужно к 1023 прибавить -4 итого получается: 1019 - это хранимая экспонента.
Нам также известна реальная мантисса - это 1,6 . Для того, чтобы получить хранимую мантиссу умножим её на 2^52: 1,6 * 2^52 = 7 205 759 403 792 793.6 после округления 7 205 759 403 792 794 . В двоичном виде: 1 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 . Отбросим старший разряд и получим хранимую мантиссу: 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010 .
Подведём итоги
В школе на уроках физики изучают, как записывать числа «в стандартном виде»:
[math]x = m \cdot 10^n[/math] ,
[math]1 \le m \lt 10,\:n\in\mathbb[/math] .
Число [math]m[/math] называют мантиссой, число [math]p[/math] называют порядком.
В компьютере в качестве основания вместо числа [math]10[/math] используется число [math]2[/math] .
Стандарт IEEE 754
IEEE 754 (IEC 60559) — широко используемый стандарт IEEE, описывающий формат представления чисел с плавающей точкой.
binary32 | binary64 | |
---|---|---|
Название | число одинарной точности | число двойной точности |
Тип в C | float | double |
Биты знака | 1 | 1 |
Биты экспоненты | 8 | 11 |
Биты мантиссы | 23 | 52 |
Мин. положит. денорм. значение | [math]1.401298464 \times 10^[/math] | [math]4.9406564584124654 \times 10^[/math] |
Макс. значение | [math]3.402823 \times 10^[/math] | [math]1.7976931348623157 \times 10^[/math] |
Число десятичных цифр | 7—8 | 15—17 |
Стандартом языка C гарантируется только, что
На большинстве платформ применяются типы с плавающей точкой в соответствии со стандартом IEEE 754.
Важным нюансом является форма записи вещественных констант разных типов. Например,
- 1 — это константа типа int,
- 1. или 1.0 — это константа типа double,
- 1.f или 1.0f — это константа типа float.
На следующих занятиях мы разберём, почему смешивать в одном выражении операнды типов float и double может быть плохо.
Представление в памяти
- знак числа
- экспонента (показатель)
- мантисса
Число одинарной точноcти (float)
Занимает в памяти 32 бита (4 байта).
Числа одинарной точности с плавающей запятой обеспечивают относительную точность 7-8 десятичных цифр в диапазоне от 10 −38 до примерно 10 38 .
[math](-1)^> \times (1.b_b_ \dots b_0)_2 \times 2^<(b_<30>b_ \dots b_)_2 - 127>[/math]
Число двойной точноcти (double)
Занимает в памяти 64 бита (8 байт).
Числа двойной точности с плавающей запятой обеспечивают точность в 15—17 десятичных цифр и масштабы в диапазоне примерно от 10 −308 до 10 308 .
Распределение
Множество вещественных чисел в математике бесконечно и даже несчётно.
Множество чисел, представимых в формате с плавающей точкой, не только счётно, но и конечно. Все эти числа являются рациональными.
Однако расстояние между соседними числами непостоянно.
Промежуток между соседними числами удваивается при переходе через каждую степень двойки.
Чем дальше от нуля, тем реже и реже встречаются на числовой прямой числа, которые представимы вещественным типом.
Точное представление
Точно представляются только несократимые дроби, у которых знаменатель является степенью двойки.
Читайте также: