Я просматривал этот пример, в котором есть функция, выводящая шестнадцатеричный битовый шаблон для представления произвольного числа с плавающей запятой.
void ExamineFloat(float fValue)
{
printf("%08lx\n", *(unsigned long *)&fValue);
}
Зачем брать адрес fValue, приводить к беззнаковому длинному указателю, а затем разыменовывать? Разве вся эта работа не эквивалентна прямому приведению к unsigned long?
printf("%08lx\n", (unsigned long)fValue);
Я попробовал, но ответ не тот, так запутался.
5 ответов
(unsigned long)fValue
Это преобразует значение float
в значение unsigned long
в соответствии с «обычными арифметическими преобразованиями».
*(unsigned long *)&fValue
Здесь цель состоит в том, чтобы взять адрес, по которому хранится fValue
, сделать вид, что по этому адресу нет float
, а unsigned long
, и затем прочитать этот unsigned long
. Цель состоит в том, чтобы исследовать битовый шаблон, который используется для хранения float
в памяти.
Как показано, это вызывает неопределенное поведение.
Причина: вы не можете получить доступ к объекту через указатель на тип, который не «совместим» с типом объекта. «Совместимые» типы - это, например, (unsigned
) char
и любой другой тип или структуры, которые имеют одни и те же начальные члены (здесь речь идет о C). См. §6.5 / 7 N1570 для получения подробной информации (C11) список ( Обратите внимание, что я использую слово «совместимый» иначе - более широко - чем в упомянутом тексте. )
Решение: преобразовать в unsigned char *
, получить доступ к отдельным байтам объекта и собрать из них unsigned long
:
unsigned long pattern = 0;
unsigned char * access = (unsigned char *)&fValue;
for (size_t i = 0; i < sizeof(float); ++i) {
pattern |= *access;
pattern <<= CHAR_BIT;
++access;
}
Обратите внимание, что (как указал @CodesInChaos) вышеупомянутое значение с плавающей запятой обрабатывается как сохраненное с его старшим байтом первым ("обратный порядок байтов"). Если ваша система использует другой порядок байтов для значений с плавающей запятой, вам нужно будет отрегулировать его (или переставить байты выше unsigned long
, как вам удобнее).
Значения с плавающей запятой имеют представление в памяти: например, байты могут представлять значение с плавающей запятой, используя IEEE 754 а>.
Первое выражение *(unsigned long *)&fValue
интерпретирует эти байты, как если бы они были представлением значения unsigned long
. Фактически, в стандарте C это приводит к неопределенному поведению (согласно так называемому «правилу строгого алиасинга»). На практике необходимо учитывать такие аспекты, как порядок байтов.
Второе выражение (unsigned long)fValue
соответствует стандарту C. Имеет точное значение:
C11 (n1570), § 6.3.1.4 Действительное число с плавающей запятой и целое число
Когда конечное значение реального плавающего типа преобразуется в целочисленный тип, отличный от
_Bool
, дробная часть отбрасывается (то есть значение усекается до нуля). Если значение интегральной части не может быть представлено целочисленным типом, поведение не определено.
*(unsigned long *)&fValue
не эквивалентно прямому приведению к unsigned long
.
Преобразование в (unsigned long)fValue
преобразует значение fValue
в unsigned long
, используя обычные правила преобразования значения float
в значение unsigned long
. Представление этого значения в unsigned long
(например, в терминах битов) может сильно отличаться от того, как это же значение представлено в float
.
Преобразование *(unsigned long *)&fValue
формально имеет неопределенное поведение. Он интерпретирует память, занимаемую fValue
, как если бы она была unsigned long
. На практике (т.е. это то, что часто случается, даже если поведение не определено), это часто дает значение, сильно отличающееся от fValue
.
Приведение типов в C выполняет как преобразование типа, так и преобразование значения. Преобразование с плавающей запятой → беззнаковое длинное число обрезает дробную часть числа с плавающей запятой и ограничивает значение возможным диапазоном длинного беззнакового числа. Преобразование одного типа указателя в другой не требует изменения значения, поэтому использование преобразования типа указателя - это способ сохранить то же представление в памяти при изменении типа, связанного с этим представлением.
В данном случае это способ вывести двоичное представление значения с плавающей запятой.
Как уже отмечали другие, приведение указателя на не-символьный тип к указателю на другой не-символьный тип, а затем разыменование является неопределенным поведением.
То, что printf("%08lx\n", *(unsigned long *)&fValue)
вызывает неопределенное поведение, не обязательно означает, что запуск программы, которая пытается выполнить такую пародию, приведет к стиранию жесткого диска или заставит носовых демонов вырваться из носа (два признака неопределенного поведения). На компьютере, на котором sizeof(unsigned long)==sizeof(float)
и на котором оба типа имеют одинаковые требования к выравниванию, этот printf
почти наверняка сделает то, что от него ожидается, а именно напечатает шестнадцатеричное представление значения с плавающей запятой. обсуждаемый.
Это не должно вызывать удивления. Стандарт C открыто предлагает реализации для расширения языка. Многие из этих расширений находятся в областях, поведение которых, строго говоря, не определено. Например, функция POSIX dlsym возвращает void*
, но эта функция обычно используется чтобы найти адрес функции, а не глобальной переменной. Это означает, что указатель void, возвращаемый dlsym
, необходимо преобразовать в указатель функции, а затем разыменовать его для вызова функции. Очевидно, что это неопределенное поведение, но оно, тем не менее, работает на любой платформе, совместимой с POSIX. Это не будет работать на машине с гарвардской архитектурой, на которой указатели на функции имеют другой размер, чем указатели на данные.
Точно так же приведение указателя на float
к указателю на целое число без знака с последующим разыменованием работает практически на любом компьютере практически с любым компилятором, в котором требования к размеру и выравниванию этого целого числа без знака такие же, как у а float
.
Тем не менее, использование unsigned long
может вызвать у вас проблемы. На моем компьютере unsigned long
имеет длину 64 бита и требует 64-битного выравнивания. Это несовместимо с поплавком. Было бы лучше использовать uint32_t
- то есть на моем компьютере.
Один из способов обойти этот беспорядок - это профсоюзный хак
typedef struct {
float fval;
uint32_t ival;
} float_uint32_t;
Назначение float_uint32_t.fval
и доступ из "float_uint32_t.ival" раньше было неопределенным поведением. Это уже не так в C. Нет известного мне компилятора для ударов носовых демонов для хака union. Это не было UB в C ++. Это было незаконно. До C ++ 11 совместимый компилятор C ++ должен был жаловаться на свою совместимость.
Еще один лучший способ обойти этот беспорядок - использовать формат %a
, который был частью стандарта C с 1999 года:
printf ("%a\n", fValue);
Это просто, легко, переносимо и не допускает неопределенного поведения. Это печатает шестнадцатеричное / двоичное представление рассматриваемого значения с плавающей запятой двойной точности. Поскольку printf
- архаичная функция, все аргументы float
преобразуются в double
до вызова printf
. Это преобразование должно быть точным в соответствии с версией стандарта C. Это точное значение можно узнать, позвонив scanf
или его сестрам.
Похожие вопросы
Связанные вопросы
Новые вопросы
c++
C++ — это язык программирования общего назначения. Изначально он разрабатывался как расширение C и имел аналогичный синтаксис, но теперь это совершенно другой язык. Используйте этот тег для вопросов о коде, который будет скомпилирован с помощью компилятора C++. Используйте тег версии для вопросов, связанных с конкретной стандартной версией [C++11], [C++14], [C++17], [C++20] или [C++23]. и т.д.