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

void ExamineFloat(float fValue)
{
    printf("%08lx\n", *(unsigned long *)&fValue);
}

Зачем брать адрес fValue, приводить к беззнаковому длинному указателю, а затем разыменовывать? Разве вся эта работа не эквивалентна прямому приведению к unsigned long?

printf("%08lx\n", (unsigned long)fValue);

Я попробовал, но ответ не тот, так запутался.

20
bobbay 4 Сен 2016 в 02:04

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, как вам удобнее).

30
Daniel Jour 4 Сен 2016 в 19:27

Значения с плавающей запятой имеют представление в памяти: например, байты могут представлять значение с плавающей запятой, используя IEEE 754 .

Первое выражение *(unsigned long *)&fValue интерпретирует эти байты, как если бы они были представлением значения unsigned long. Фактически, в стандарте C это приводит к неопределенному поведению (согласно так называемому «правилу строгого алиасинга»). На практике необходимо учитывать такие аспекты, как порядок байтов.

Второе выражение (unsigned long)fValue соответствует стандарту C. Имеет точное значение:

C11 (n1570), § 6.3.1.4 Действительное число с плавающей запятой и целое число

Когда конечное значение реального плавающего типа преобразуется в целочисленный тип, отличный от _Bool, дробная часть отбрасывается (то есть значение усекается до нуля). Если значение интегральной части не может быть представлено целочисленным типом, поведение не определено.

4
Community 20 Июн 2020 в 09:12

*(unsigned long *)&fValue не эквивалентно прямому приведению к unsigned long.

Преобразование в (unsigned long)fValue преобразует значение fValue в unsigned long, используя обычные правила преобразования значения float в значение unsigned long. Представление этого значения в unsigned long (например, в терминах битов) может сильно отличаться от того, как это же значение представлено в float.

Преобразование *(unsigned long *)&fValue формально имеет неопределенное поведение. Он интерпретирует память, занимаемую fValue, как если бы она была unsigned long. На практике (т.е. это то, что часто случается, даже если поведение не определено), это часто дает значение, сильно отличающееся от fValue.

4
Peter 3 Сен 2016 в 23:25

Приведение типов в C выполняет как преобразование типа, так и преобразование значения. Преобразование с плавающей запятой → беззнаковое длинное число обрезает дробную часть числа с плавающей запятой и ограничивает значение возможным диапазоном длинного беззнакового числа. Преобразование одного типа указателя в другой не требует изменения значения, поэтому использование преобразования типа указателя - это способ сохранить то же представление в памяти при изменении типа, связанного с этим представлением.

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

3
J Earls 3 Сен 2016 в 23:11

Как уже отмечали другие, приведение указателя на не-символьный тип к указателю на другой не-символьный тип, а затем разыменование является неопределенным поведением.

То, что 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 или его сестрам.

1
David Hammen 5 Сен 2016 в 23:15