b0lt уже объяснил, как работает смещение. Возможно, вы хотели бы знать, почему почему используют здесь предвзятое представление, хотя практически все современные компьютеры используют два дополнения по существу везде (и даже машины, которые не используют два дополнения, используют свое дополнение или знак -магнность, а не смещение).
Одна из целей стандартов с плавающей точкой IEEE заключалась в том, что вы могли обрабатывать биты числа с плавающей запятой как (подписанное) целое число того же размера, и если бы вы сравнили их таким образом, значения будут сортироваться в тот же порядок, что и числа с плавающей запятой.
Если вы использовали представление в дополнительном коде для показателя, малое положительное число (то есть, с отрицательным показателем) будет выглядеть очень большой целое, потому что второй MSB будет установлен. Вместо этого вместо этого используется представление смещения, вы не сталкиваетесь с этим: меньший показатель числа чисел с плавающей запятой всегда выглядит как меньшее целое число.
FWIW, это также почему числа с плавающей запятой, как правило, расположены со знаком сначала, затем с показателем и, наконец, значащим в младших значащих битах - таким образом, вы можете принимать положительные числа с плавающей запятой, обрабатывать эти биты как целые числа, и сортировать их. Когда вы это сделаете, результат будет иметь числа с плавающей запятой в правильном порядке. Например:
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
// some arbitrary floating point values
std::vector<double> vals = { 1e21, 1, 2.2, 2, 123, 1.1, 0.0001, 3, 17 };
std::vector<long long> ivals;
// Take those floating point values, and treat the bits as integers:
for (auto &&v : vals)
ivals.push_back(*reinterpret_cast<long long *>(&v));
// Sort them as integers:
std::sort(ivals.begin(), ivals.end());
// Print out both the integers and the floating point value those bits represent:
for (auto &&i : ivals)
std::cout << i << "\t(" << *reinterpret_cast<double *>(&i) << ")\n";
}
Когда мы запускаем этот результат выглядит следующим образом:
4547007122018943789 (0.0001)
4607182418800017408 (1)
4607632778762754458 (1.1)
4611686018427387904 (2)
4612136378390124954 (2.2)
4613937818241073152 (3)
4625478292286210048 (17)
4638355772470722560 (123)
4921056587992461136 (1e+21)
Как вы можете видеть, даже если мы отсортировали их как целые числа, числа с плавающей запятой, что эти биты представляют также выходят в правильном порядке.
У этого есть ограничения относительно чисел с плавающей запятой. Хотя все (не древние) компьютеры согласны с представлением положительных чисел, существуют три представления, которые (довольно недавно) использовались для подписанных чисел: знаковая величина, дополнение к одному и дополнение к двум.
Простое рассмотрение битов как целого числа, и сравнение будет отлично работать на компьютере, использующем знаковое представление величины для целых чисел. Для компьютеров, которые используют дополнение своего дополнения или два, отрицательные числа будут сортироваться в инвертированном порядке. Поскольку это все еще простое правило, довольно легко написать код, который работает с ним.Если мы изменим sort
вызов выше что-то вроде этого:
std::sort(ivals.begin(), ivals.end(),
[](auto a, auto b) { if (a < 0.0 && b < 0.0) return b < a; return a < b; }
);
... это будет правильно сортировать как положительные, так и отрицательные числа. Например, вход:
std::vector<double> vals = { 1e21, 1, 2.2, 2, 123, 1.1, 0.0001, 3, 17, -0.001, -0.00101, -1e22 };
произведет результат:
-4287162073302051438 (-1e+22)
-4661071411077222194 (-0.00101)
-4661117527937406468 (-0.001)
4547007122018943789 (0.0001)
4607182418800017408 (1)
4607632778762754458 (1.1)
4611686018427387904 (2)
4612136378390124954 (2.2)
4613937818241073152 (3)
4625478292286210048 (17)
4638355772470722560 (123)
4921056587992461136 (1e+21)
Еще одно интересное чтение, связанное с этим вопросом, - это статья Википедии: https://en.wikipedia.org/wiki/IEEE_754-1985 – nbro