- Использование union для получения частей float(Impl. defined)
- Использование преобразования указателей для получения частей float
- Сравнение float чисел
- Составление +-inf и NaN руками
- num != (int)(float)num
youtube - https://youtu.be/cRToBPl9yz0
vk - https://vk.com/video-221776054_456239041
Фиксируем точку в каком-то месте и делаем следующее:
2 ^ i + 2 ^ (i - 1) + ... + 2 ^ 0 + 2 ^ -1 + 2 ^ -2 ....
Пусть у нас числа 8 битные и точку мы ставим после первых 4х бит:
2.5 -> 0010.1000
3.125 -> 0011.0010
Но мы не можем точно представить это число, поэтому будет ошибка.
0.2 -> 0000.0011 [00110011] -> 0.1875
Ошибка составила 0.0125
Идея такая же как в научной нотации: 1.6E-35
(-1)^S * M * 2^E
, где
M - мантисса (числа дробной части)
S - бит знака
E - экспонента (на сколько нужно умножить мантиссу)
Имеют вид 1.xxxx * 2^yyyy (1 стоит на первом месте. В бинарном виде мы знаем, что она там стоит и не храним явно)
Имеют вид 0.xxxxx * 2^0000 (Если экспонента состоит из 0, то в начало добавляется не 1, а 0. Нужно для представления близких к 0 чисел)
Экспонента | Мантисса | Значение |
---|---|---|
11111111 | все нули | бесконечность (+ или -) |
11111111 | не все нули | not a number (NaN) |
00000000 | все нули | ноль |
00000000 | не все нули | денормализованный вид |
- 0 в float = 0 в int
- Сравнивать можно как инты (с некоторыми оговорками)
Оговорки:
- Нужно проверить бит знака
- -0 = 0
- корректно обрабатывать NaN (NaN != NaN)
- Остальное корректно: денормализованныые < нормализованные < бесконечность
Стандартно округлятся к ближайшему четному, чтобы округление было устойчевым. Если все время округлять вниз, то значение будет уменьшаться каждую операцию.
Значение | 1.40 | 1.60 | 1.50 | 2.50 | -1.50 |
---|---|---|---|---|---|
К нулю (towards 0) | 1 | 1 | 1 | 2 | -1 |
Вниз (−inf) | 1 | 1 | 1 | 2 | -2 |
Вверх (+inf) | 2 | 2 | 2 | 3 | -1 |
К ближайшему целому вверх | 1 | 2 | 2 | 3 | -1 |
Ближайшее четное (умолчанию) | 1 | 2 | 2 | 2 | -2 |
Format | Sign bits | Exponent bits | Mantissa bits | Exponent bias |
---|---|---|---|---|
32 bit | 1 | 8 | 23 (+1) | 2^(8-1)-1=127 |
64 bit | 1 | 11 | 52 (+1) | 2^(11-1)-1=1023 |
11.8125 -> ?
- Определим бит знака.
В нашем случае 0.
- Переведем его в двоичное дробное число как обычно: целую часть делим на 2, дробную умножаем на 2.
целая часть (смотрим остатки):
11 / 2 = 5 [1]
5 / 2 = 2 [1]
2 / 2 = 1 [0]
1 / 2 = 0 [1]
дробная часть (смотрим переход через разряд):
8125 * 2 = 6250 [1]
6250 * 2 = 2500 [1]
2500 * 2 = 5000 [0]
5000 * 2 = 0 [1]
Результат (собираем снизу вверх сначала целую часть потом дробную):
1011.1101
- Нормализуем (считаем, на сколько нужно передвинуть точку, чтобы единственная цифра перед точкой была 1)
В нашем случае нужно сдвинуть на 3 влево. Это будет предварительное значение экспоненты. Если бы нужно было двигать вправо, то предварительная экспонента была бы отрицательной.
Результат:
1.0111101 * 2 ^ 3
- Считаем смещенную экспоненту.
Нам нужно уметь хранить отрицательные экспоненты. Поэтому сначала идут 127 отрицательных экспонент, затем положительные. Поэтому к экспоненте нужно прибавить сдвиг (bias) = 127.
Результат:
1.0111101 * 2 ^ 130
- Убираем первую 1.
Так как мы договорились, что у нас первая всегда 1, то нет смысла ее хранить, можно ее просто подразумевать там.
Результат:
0111101 * 2 ^ 130
Итог:
sign = 0
mantissa = 0111101
exp = 130 = 10000010
-114.3 -> ?
- Определим бит знака.
В нашем случае 1.
- Переведем его в двоичное дробное число как обычно: целую часть делим на 2, дробную умножаем на 2.
целая часть (смотрим остатки):
114 / 2 = 57 [0]
57 / 2 = 28 [1]
28 / 2 = 14 [0]
14 / 2 = 7 [0]
7 / 2 = 3 [1]
3 / 2 = 1 [1]
1 / 2 = 0 [1]
дробная часть (смотрим переход через разряд):
3 * 2 = 6 [0]
6 * 2 = 2 [1]
2 * 2 = 4 [0]
4 * 2 = 8 [0]
8 * 2 = 6 [1]
Зациклились 1001
Результат:
1110010.0100110011001....
- Нормализуем (считаем, на сколько нужно передвинуть точку, чтобы единственная цифра перед точкой была 1)
В нашем случае нужно сдвинуть на 6 влево. Это будет предварительное значение экспоненты. Если бы нужно было двигать вправо, то предварительная экспонента была бы отрицательной.
Результат:
1.1100100100110011001... * 2 ^ 6
- Считаем смещенную экспоненту.
Нам нужно уметь хранить отрицательные экспоненты. Поэтому сначала идут 127 отрицательных экспонент, затем положительные. Поэтому к экспоненте нужно прибавить сдвиг (bias) = 127.
Результат:
1.1100100100110011001... * 2 ^ 133
- Убираем первую 1.
Так как мы договорились, что у нас первая всегда 1, то нет смысла ее хранить, можно ее просто подразумевать там.
Результат:
1100100100110011001... * 2 ^ 133
- Приводим мантиссу к 23 битному виду
Результат:
11001001001100110011001 * 2 ^ 133
- Округление
Округляем к ближайшему четному. Нужно посмотреть, какой бит шел бы следующим (в нашем случае 1). Если 1, то к результату прибавляем 1. иначе ничего не делаем.
Результат:
11001001001100110011010 * 2 ^ 133
Итог:
sign = 1
mantissa = 11001001001100110011010
exp = 133 = 10000101
#include <math.h>
компилировать с -lm
Функции из библиотеки:
int isinf(float) - вернет 0 если не inf, любое другое число если inf
int isnan(float) - аналогично про NaN
int isfinite(float) - вернет 0 если inf, любое другое число если не inf
int isnormal(float) - нормализованное ли число
int fpclassify(float) - классифицирует float и вернет одну из констант:
FP_INFINITE Число inf
FP_NAN Число - NaN Indicates that x is not a number (NaN).
FP_NORMAL Число нормализованное
FP_SUBNORMAL Число денормализованное
FP_ZERO Число 0
Обратите внимание, что поведение, описанное тут является implementation defined. Это означает, что на него нельзя полагаться в общем виде.
typedef union {
float f;
struct {
// implementation defined поведение заключается в порядке бит.
// нет строгих гарантий, что сначала будут идти 23, потом 8, а потом 1.
unsigned int mantissa : 23;
unsigned int exponent : 8;
unsigned int sign : 1;
} parts;
} float_uni;
Об этом сказано в стандарте C11: 6.7.2.1, пункт 11, также об этом можно почитать на cppreference в разделе Notes.
Для gcc есть некоторые гарантии того, что поведение такое, как мы ожидаем.
И пример такого кода есть в glibc или его можно самостоятельно увидеть по такому пути: /usr/include/x86_64-linux-gnu/ieee754.h
.
Однако, такой код трудно переносится на другие платформы (нужно дополнительно искать подтверждения того, что код будет работать ожидаемо) и поведение может меняться.
В контест этот код приниматься не будет относитесь к нему как к интересному факту